From b3491520bda174d5233ca997055ace20b50b4360 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 17 Aug 2026 20:07:14 +0300 Subject: [PATCH] DEV-000069 general agent delegation codex bridge foundation --- adapters/postgres/continuity_repository.go | 80 ++++- adapters/postgres/work_repository.go | 218 ++++++++++++ ...7-general-agent-delegation-codex-bridge.md | 54 +++ cmd/continuity-runtime/attention.go | 60 ++++ cmd/core-runtime/capability_chat.go | 126 +++++++ cmd/core-runtime/capability_chat_test.go | 28 ++ cmd/core-runtime/learning.go | 51 +++ cmd/core-runtime/main.go | 278 +++++++++++++++ cmd/creator-web/server.go | 19 +- cmd/creator-web/static/assets/chat.js | 20 ++ contracts/continuity/types.go | 16 + contracts/interaction/types.go | 2 + contracts/work/types.go | 120 +++++++ .../024_work_agent_delegation_foundation.sql | 145 ++++++++ ..._delegation_attention_and_verification.sql | 60 ++++ dev/ACTIVE_TASK.json | 4 +- ...general-agent-delegation-codex-bridge.json | 163 +++++++++ modules/continuity/domain/human_response.go | 13 +- .../continuity/domain/human_response_test.go | 36 ++ modules/work/domain/delegation.go | 326 ++++++++++++++++++ modules/work/domain/delegation_test.go | 115 ++++++ 21 files changed, 1914 insertions(+), 20 deletions(-) create mode 100644 architecture/adrs/ADR-0017-general-agent-delegation-codex-bridge.md create mode 100644 database/migrations/024_work_agent_delegation_foundation.sql create mode 100644 database/migrations/025_delegation_attention_and_verification.sql create mode 100644 dev/tasks/DEV-000069-general-agent-delegation-codex-bridge.json create mode 100644 modules/work/domain/delegation.go create mode 100644 modules/work/domain/delegation_test.go diff --git a/adapters/postgres/continuity_repository.go b/adapters/postgres/continuity_repository.go index 67b4b81..8989c26 100644 --- a/adapters/postgres/continuity_repository.go +++ b/adapters/postgres/continuity_repository.go @@ -162,10 +162,10 @@ func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continui return continuity.ProspectiveItem{}, continuity.AttentionItem{}, err } err = tx.QueryRow(ctx, `INSERT INTO continuity.attention_items ( - attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary, + attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary, due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id, action_attempt_id,delivery_outcome,response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,'','','','','','','','',0,1) + VALUES ($1,$2,$3,$4,$5,'',$6,$7,$8,$9,$10,0,'','','','','','','','',0,1) ON CONFLICT (source_kind, source_prospective_item_id) DO UPDATE SET state=EXCLUDED.state, due_at_unix_millis=EXCLUDED.due_at_unix_millis, @@ -181,12 +181,12 @@ func (r *ContinuityRepository) ClaimDueReminder(ctx context.Context, id continui response_outcome='', responded_at_unix_millis=0, revision=continuity.attention_items.revision+1 - RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis, + RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis, created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`, attention.ID, attention.OwnerPrincipalID, attention.ScopeID, attention.SourceKind, attention.SourceProspectiveID, attention.Level, attention.State, attention.Summary, attention.DueAtUnixMillis, attention.CreatedAtUnixMillis).Scan( - &attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID, + &attention.ID, &attention.OwnerPrincipalID, &attention.ScopeID, &attention.SourceKind, &attention.SourceProspectiveID, &attention.SourceExternalRef, &attention.Level, &attention.State, &attention.Summary, &attention.DueAtUnixMillis, &attention.CreatedAtUnixMillis, &attention.DeliveredAtUnixMillis, &attention.ActionIntentID, &attention.AuthorizedActionID, &attention.ActionAttemptID, &attention.DeliveryOutcome, &attention.ResponseID, &attention.ResponseUtteranceID, &attention.ResponseOperation, @@ -204,7 +204,7 @@ func (r *ContinuityRepository) PendingAttention(ctx context.Context, limit int) if limit < 1 || limit > 100 { return nil, fmt.Errorf("invalid attention limit") } - rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary, + rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary, due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision FROM continuity.attention_items WHERE state=$1 ORDER BY due_at_unix_millis, attention_item_id LIMIT $2`, continuity.AttentionPending, limit) @@ -219,7 +219,7 @@ func (r *ContinuityRepository) AttentionByOwner(ctx context.Context, owner ident if owner == "" || limit < 1 || limit > 100 { return nil, fmt.Errorf("invalid attention owner query") } - rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary, + rows, err := r.pool.Query(ctx, `SELECT attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary, due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision FROM continuity.attention_items WHERE owner_principal_id=$1 ORDER BY due_at_unix_millis DESC, attention_item_id DESC LIMIT $2`, owner, limit) @@ -235,7 +235,7 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide return nil, fmt.Errorf("invalid response candidate query") } rows, err := r.pool.Query(ctx, `SELECT - COALESCE(a.attention_item_id,''),COALESCE(a.owner_principal_id,''),COALESCE(a.scope_id,''),COALESCE(a.source_kind,''),COALESCE(a.source_prospective_item_id,''),COALESCE(a.level,''),COALESCE(a.state,''),COALESCE(a.summary,''), + COALESCE(a.attention_item_id,''),COALESCE(a.owner_principal_id,''),COALESCE(a.scope_id,''),COALESCE(a.source_kind,''),COALESCE(a.source_prospective_item_id,''),COALESCE(a.source_external_ref,''),COALESCE(a.level,''),COALESCE(a.state,''),COALESCE(a.summary,''), COALESCE(a.due_at_unix_millis,0),COALESCE(a.created_at_unix_millis,0),COALESCE(a.delivered_at_unix_millis,0),COALESCE(a.action_intent_id,''),COALESCE(a.authorized_action_id,''),COALESCE(a.action_attempt_id,''),COALESCE(a.delivery_outcome,''), COALESCE(a.response_id,''),COALESCE(a.response_utterance_id,''),COALESCE(a.response_operation,''),COALESCE(a.response_outcome,''),COALESCE(a.responded_at_unix_millis,0),COALESCE(a.revision,0), p.prospective_item_id,p.commitment_id,p.owner_principal_id,p.scope_id,p.goal_id,p.source_intent_id,p.source_utterance_id,p.summary,p.trigger_at_unix_millis,p.state,p.revision @@ -255,12 +255,12 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide var candidates []continuitydomain.ResponseCandidate for rows.Next() { var candidate continuitydomain.ResponseCandidate - var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, level, attentionState, attentionSummary string + var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, sourceExternalRef, level, attentionState, attentionSummary string var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string var dueAt, createdAt, deliveredAt, respondedAt int64 var attentionRevision uint64 if err := rows.Scan( - &attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &level, &attentionState, &attentionSummary, + &attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &sourceExternalRef, &level, &attentionState, &attentionSummary, &dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome, &responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision, &candidate.Prospective.ID, &candidate.Prospective.CommitmentID, &candidate.Prospective.OwnerPrincipalID, @@ -276,6 +276,7 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide ScopeID: authority.ScopeID(attentionScope), SourceKind: continuity.AttentionSourceKind(sourceKind), SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective), + SourceExternalRef: sourceExternalRef, Level: continuity.AttentionLevel(level), State: continuity.AttentionState(attentionState), Summary: attentionSummary, @@ -378,19 +379,66 @@ func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { return err } + case continuity.ResponseContinue, continuity.ResponseReject: + if response.TargetAttentionID == "" { + return ErrAttentionNotFound + } + if err = markAttentionResponded(ctx, tx, response, continuity.AttentionResolved); err != nil { + return err + } } return tx.Commit(ctx) } +func (r *ContinuityRepository) CreateAttentionItem(ctx context.Context, item continuity.AttentionItem) (continuity.AttentionItem, error) { + err := r.pool.QueryRow(ctx, `INSERT INTO continuity.attention_items( + attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary, + due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, + response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) + ON CONFLICT (source_kind, source_external_ref) WHERE source_external_ref <> '' DO UPDATE SET + level=EXCLUDED.level, + state=EXCLUDED.state, + summary=EXCLUDED.summary, + due_at_unix_millis=EXCLUDED.due_at_unix_millis, + created_at_unix_millis=EXCLUDED.created_at_unix_millis, + delivered_at_unix_millis=0, + action_intent_id='', + authorized_action_id='', + action_attempt_id='', + delivery_outcome='', + response_id='', + response_utterance_id='', + response_operation='', + response_outcome='', + responded_at_unix_millis=0, + revision=continuity.attention_items.revision+1 + RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis, + created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, + response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`, + item.ID, item.OwnerPrincipalID, item.ScopeID, item.SourceKind, item.SourceProspectiveID, item.SourceExternalRef, item.Level, item.State, + item.Summary, item.DueAtUnixMillis, item.CreatedAtUnixMillis, item.DeliveredAtUnixMillis, item.ActionIntentID, item.AuthorizedActionID, + item.ActionAttemptID, item.DeliveryOutcome, item.ResponseID, item.ResponseUtteranceID, item.ResponseOperation, item.ResponseOutcome, + item.RespondedAtUnixMillis, item.Revision).Scan( + &item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State, + &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, + &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation, &item.ResponseOutcome, + &item.RespondedAtUnixMillis, &item.Revision) + if err != nil { + return continuity.AttentionItem{}, fmt.Errorf("create attention item: %w", err) + } + return item, nil +} + func (r *ContinuityRepository) MarkAttentionDelivering(ctx context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) { var item continuity.AttentionItem err := r.pool.QueryRow(ctx, `UPDATE continuity.attention_items SET state=$1, revision=revision+1 WHERE attention_item_id=$2 AND state=$3 AND revision=$4 - RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis, + RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis, created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`, continuity.AttentionDelivering, id, continuity.AttentionPending, revision).Scan( - &item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State, + &item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision) @@ -406,12 +454,12 @@ func (r *ContinuityRepository) MarkAttentionDelivered(ctx context.Context, id co SET state=$1, revision=revision+1, delivered_at_unix_millis=$2, action_intent_id=$3, authorized_action_id=$4, action_attempt_id=$5, delivery_outcome=$6 WHERE attention_item_id=$7 AND state=$8 AND revision=$9 - RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis, + RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis, created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`, continuity.AttentionDelivered, deliveredAtUnixMillis, intentID, authorizedID, attemptID, outcome, id, continuity.AttentionDelivering, revision).Scan( - &item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State, + &item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision) @@ -427,12 +475,12 @@ func (r *ContinuityRepository) MarkAttentionFailed(ctx context.Context, id conti SET state=$1, revision=revision+1, action_intent_id=$2, authorized_action_id=$3, action_attempt_id=$4, delivery_outcome=$5 WHERE attention_item_id=$6 AND state=$7 AND revision=$8 - RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,level,state,summary,due_at_unix_millis, + RETURNING attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,due_at_unix_millis, created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome, response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision`, continuity.AttentionFailed, intentID, authorizedID, attemptID, outcome, id, continuity.AttentionDelivering, revision).Scan( - &item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.Level, &item.State, + &item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation, &item.ResponseOutcome, &item.RespondedAtUnixMillis, &item.Revision) @@ -541,7 +589,7 @@ func scanAttentionRows(rows continuityRows) ([]continuity.AttentionItem, error) items := make([]continuity.AttentionItem, 0) for rows.Next() { var item continuity.AttentionItem - if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, + if err := rows.Scan(&item.ID, &item.OwnerPrincipalID, &item.ScopeID, &item.SourceKind, &item.SourceProspectiveID, &item.SourceExternalRef, &item.Level, &item.State, &item.Summary, &item.DueAtUnixMillis, &item.CreatedAtUnixMillis, &item.DeliveredAtUnixMillis, &item.ActionIntentID, &item.AuthorizedActionID, &item.ActionAttemptID, &item.DeliveryOutcome, &item.ResponseID, &item.ResponseUtteranceID, &item.ResponseOperation, diff --git a/adapters/postgres/work_repository.go b/adapters/postgres/work_repository.go index 50600de..f332f0e 100644 --- a/adapters/postgres/work_repository.go +++ b/adapters/postgres/work_repository.go @@ -74,3 +74,221 @@ func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.Pla } return tx.Commit(ctx) } + +func (r *WorkRepository) SaveDelegationBundle(ctx context.Context, delegation work.Delegation, run work.AgentRun) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + if _, err = tx.Exec(ctx, `INSERT INTO work.delegations( + delegation_id,goal_id,work_plan_id,plan_candidate_id,capability_requirement_id,capability_kind, + selected_implementation_id,selected_implementation_revision,selected_implementation_kind,protocol, + source_intent_id,source_utterance_id,conversation_id,project_id,project_display_name,desired_outcome, + constraint_summary,verification_summary,context_summary,context_refs,state,latest_run_id,latest_result_id, + created_at_unix_millis,updated_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25) + ON CONFLICT(delegation_id) DO NOTHING`, + delegation.ID, delegation.GoalID, delegation.PlanID, delegation.PlanCandidateID, delegation.CapabilityRequirementID, + delegation.CapabilityKind, delegation.SelectedImplementationID, delegation.SelectedImplementationRev, + delegation.SelectedImplementationKind, delegation.Protocol, delegation.SourceIntentID, delegation.SourceUtteranceID, + delegation.ConversationID, delegation.ProjectID, delegation.ProjectDisplayName, delegation.DesiredOutcome, + delegation.ConstraintSummary, delegation.VerificationSummary, delegation.ContextSummary, delegation.ContextRefs, + delegation.State, delegation.LatestRunID, delegation.LatestResultID, delegation.CreatedAtUnixMillis, delegation.UpdatedAtUnixMillis); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO work.agent_runs( + run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary, + started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(run_id) DO NOTHING`, + run.ID, run.DelegationID, run.ImplementationID, run.ImplementationRevision, run.Protocol, run.State, + run.DerivedInstructionSummary, run.StartedAtUnixMillis, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil { + return err + } + return tx.Commit(ctx) +} + +func (r *WorkRepository) SaveAgentCheckpoint(ctx context.Context, run work.AgentRun, checkpoint work.AgentCheckpoint) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + if _, err = tx.Exec(ctx, `UPDATE work.agent_runs + SET state=$2,updated_at_unix_millis=$3,completed_at_unix_millis=$4 + WHERE run_id=$1`, run.ID, run.State, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil { + return err + } + if _, err = tx.Exec(ctx, `UPDATE work.delegations + SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4 + WHERE delegation_id=$1`, checkpoint.DelegationID, delegationStateForRun(run.State), checkpoint.RecordedAtUnixMillis, run.ID); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO work.agent_checkpoints( + checkpoint_id,delegation_id,run_id,state,blocker_class,completed_summary,remaining_summary,test_summary, + branch_name,commit_hash,next_step,artifact_refs,recorded_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(checkpoint_id) DO NOTHING`, + checkpoint.ID, checkpoint.DelegationID, checkpoint.RunID, checkpoint.State, checkpoint.BlockerClass, + checkpoint.CompletedSummary, checkpoint.RemainingSummary, checkpoint.TestSummary, checkpoint.BranchName, + checkpoint.CommitHash, checkpoint.NextStep, checkpoint.ArtifactRefs, checkpoint.RecordedAtUnixMillis); err != nil { + return err + } + return tx.Commit(ctx) +} + +func (r *WorkRepository) SaveAgentResult(ctx context.Context, run work.AgentRun, result work.AgentResult) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + if _, err = tx.Exec(ctx, `UPDATE work.agent_runs + SET state=$2,updated_at_unix_millis=$3,completed_at_unix_millis=$4 + WHERE run_id=$1`, run.ID, run.State, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil { + return err + } + if _, err = tx.Exec(ctx, `UPDATE work.delegations + SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4,latest_result_id=$5 + WHERE delegation_id=$1`, result.DelegationID, work.DelegationVerifying, result.RecordedAtUnixMillis, run.ID, result.ID); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO work.agent_results( + result_id,delegation_id,run_id,state,summary,outcome_ref,verification_required,verification_summary,artifact_refs,recorded_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(result_id) DO NOTHING`, + result.ID, result.DelegationID, result.RunID, result.State, result.Summary, result.OutcomeRef, result.VerificationRequired, + result.VerificationSummary, result.ArtifactRefs, result.RecordedAtUnixMillis); err != nil { + return err + } + return tx.Commit(ctx) +} + +func (r *WorkRepository) SaveResumedRun(ctx context.Context, delegation work.Delegation, run work.AgentRun) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + if _, err = tx.Exec(ctx, `UPDATE work.delegations + SET state=$2,updated_at_unix_millis=$3,latest_run_id=$4 + WHERE delegation_id=$1`, delegation.ID, delegation.State, delegation.UpdatedAtUnixMillis, delegation.LatestRunID); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO work.agent_runs( + run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary, + started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(run_id) DO NOTHING`, + run.ID, run.DelegationID, run.ImplementationID, run.ImplementationRevision, run.Protocol, run.State, + run.DerivedInstructionSummary, run.StartedAtUnixMillis, run.UpdatedAtUnixMillis, run.CompletedAtUnixMillis); err != nil { + return err + } + return tx.Commit(ctx) +} + +func (r *WorkRepository) SaveDelegationVerification(ctx context.Context, delegation work.Delegation, verification work.DelegationVerification) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + if _, err = tx.Exec(ctx, `UPDATE work.delegations + SET state=$2,updated_at_unix_millis=$3 + WHERE delegation_id=$1`, delegation.ID, delegation.State, delegation.UpdatedAtUnixMillis); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO work.delegation_verifications( + verification_id,delegation_id,run_id,result_id,verified,summary,evidence_refs,recorded_at_unix_millis) + VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(verification_id) DO NOTHING`, + verification.ID, verification.DelegationID, verification.RunID, verification.ResultID, verification.Verified, + verification.Summary, verification.EvidenceRefs, verification.RecordedAtUnixMillis); err != nil { + return err + } + return tx.Commit(ctx) +} + +func (r *WorkRepository) LatestAgentResult(ctx context.Context, delegationID work.DelegationID) (work.AgentResult, error) { + var value work.AgentResult + err := r.pool.QueryRow(ctx, `SELECT result_id,delegation_id,run_id,state,summary,outcome_ref,verification_required,verification_summary,artifact_refs,recorded_at_unix_millis + FROM work.agent_results WHERE delegation_id=$1 ORDER BY recorded_at_unix_millis DESC, result_id DESC LIMIT 1`, delegationID). + Scan(&value.ID, &value.DelegationID, &value.RunID, &value.State, &value.Summary, &value.OutcomeRef, &value.VerificationRequired, &value.VerificationSummary, &value.ArtifactRefs, &value.RecordedAtUnixMillis) + if err != nil { + return work.AgentResult{}, fmt.Errorf("load latest agent result: %w", err) + } + return value, nil +} + +func (r *WorkRepository) CountAgentRuns(ctx context.Context, delegationID work.DelegationID) (int, error) { + var count int + err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM work.agent_runs WHERE delegation_id=$1`, delegationID).Scan(&count) + if err != nil { + return 0, fmt.Errorf("count agent runs: %w", err) + } + return count, nil +} + +func (r *WorkRepository) LatestAgentCheckpoint(ctx context.Context, delegationID work.DelegationID) (work.AgentCheckpoint, error) { + var value work.AgentCheckpoint + err := r.pool.QueryRow(ctx, `SELECT checkpoint_id,delegation_id,run_id,state,blocker_class,completed_summary,remaining_summary,test_summary,branch_name,commit_hash,next_step,artifact_refs,recorded_at_unix_millis + FROM work.agent_checkpoints WHERE delegation_id=$1 ORDER BY recorded_at_unix_millis DESC, checkpoint_id DESC LIMIT 1`, delegationID). + Scan(&value.ID, &value.DelegationID, &value.RunID, &value.State, &value.BlockerClass, &value.CompletedSummary, &value.RemainingSummary, + &value.TestSummary, &value.BranchName, &value.CommitHash, &value.NextStep, &value.ArtifactRefs, &value.RecordedAtUnixMillis) + if err != nil { + return work.AgentCheckpoint{}, err + } + return value, nil +} + +func (r *WorkRepository) LoadDelegation(ctx context.Context, id work.DelegationID) (work.Delegation, error) { + var value work.Delegation + err := r.pool.QueryRow(ctx, `SELECT delegation_id,goal_id,work_plan_id,plan_candidate_id,capability_requirement_id,capability_kind, + selected_implementation_id,selected_implementation_revision,selected_implementation_kind,protocol, + source_intent_id,source_utterance_id,conversation_id,project_id,project_display_name,desired_outcome,constraint_summary, + verification_summary,context_summary,context_refs,state,latest_run_id,latest_result_id,created_at_unix_millis,updated_at_unix_millis + FROM work.delegations WHERE delegation_id=$1`, id). + Scan(&value.ID, &value.GoalID, &value.PlanID, &value.PlanCandidateID, &value.CapabilityRequirementID, &value.CapabilityKind, + &value.SelectedImplementationID, &value.SelectedImplementationRev, &value.SelectedImplementationKind, &value.Protocol, + &value.SourceIntentID, &value.SourceUtteranceID, &value.ConversationID, &value.ProjectID, &value.ProjectDisplayName, + &value.DesiredOutcome, &value.ConstraintSummary, &value.VerificationSummary, &value.ContextSummary, &value.ContextRefs, + &value.State, &value.LatestRunID, &value.LatestResultID, &value.CreatedAtUnixMillis, &value.UpdatedAtUnixMillis) + if err != nil { + return work.Delegation{}, fmt.Errorf("load delegation: %w", err) + } + return value, nil +} + +func (r *WorkRepository) LoadAgentRun(ctx context.Context, id work.AgentRunID) (work.AgentRun, error) { + var value work.AgentRun + err := r.pool.QueryRow(ctx, `SELECT run_id,delegation_id,implementation_id,implementation_revision,protocol,state,derived_instruction_summary,started_at_unix_millis,updated_at_unix_millis,completed_at_unix_millis + FROM work.agent_runs WHERE run_id=$1`, id). + Scan(&value.ID, &value.DelegationID, &value.ImplementationID, &value.ImplementationRevision, &value.Protocol, &value.State, &value.DerivedInstructionSummary, + &value.StartedAtUnixMillis, &value.UpdatedAtUnixMillis, &value.CompletedAtUnixMillis) + if err != nil { + return work.AgentRun{}, fmt.Errorf("load agent run: %w", err) + } + return value, nil +} + +func delegationStateForRun(state work.AgentRunState) work.DelegationState { + switch state { + case work.AgentRunStarted, work.AgentRunRunning: + return work.DelegationRunning + case work.AgentRunWaiting, work.AgentRunCheckpointed, work.AgentRunExecutionWindowExhausted: + return work.DelegationWaiting + case work.AgentRunBlocked: + return work.DelegationBlocked + case work.AgentRunCompleted: + return work.DelegationVerifying + case work.AgentRunCancelled: + return work.DelegationCancelled + case work.AgentRunFailed: + return work.DelegationFailed + default: + return work.DelegationPending + } +} + +func latestRowErr(err error) error { + if err == nil { + return nil + } + return err +} diff --git a/architecture/adrs/ADR-0017-general-agent-delegation-codex-bridge.md b/architecture/adrs/ADR-0017-general-agent-delegation-codex-bridge.md new file mode 100644 index 0000000..23ea9e6 --- /dev/null +++ b/architecture/adrs/ADR-0017-general-agent-delegation-codex-bridge.md @@ -0,0 +1,54 @@ +# ADR-0017 — General Agent Delegation and Codex Bridge V1 + +Status: Accepted + +Task: DEV-000069 + +## Context + +Domovoy already has Human Interaction, Situation/context, optional durable Work/Plan, Capability registry/resolution, Continuity/Attention, HumanResponse, Experience/Reflection, Skill evolution, Secret Resolver and Cognitive Gateway/Router. The next step is not "make Codex the center", but to let Domovoy delegate suitable work to a general external intelligent agent while preserving canonical human intent, work semantics, authority boundaries and future portability to non-Codex agents. + +The first real implementation is Codex for software-engineering work. However, a model is not an external agent. Cognitive models provide advice/candidates, while an external agent may inspect state, use tools, modify files, run tests, maintain a long-running task, checkpoint, resume and return evidence-bearing results. + +## Decision + +1. Do not create a new canonical owner for DEV-000069. +2. `capability` remains the canonical owner for agent implementation registration by reusing `CapabilityImplementation` with `Kind=AGENT`. A Codex bridge is one concrete implementation, not a new canonical capability identity. +3. `work` remains the canonical owner for durable delegation lifecycle. DEV-000069 extends Work semantics from planning-only `DelegationCandidate` to real delegation execution state such as `Delegation`, `AgentRun`, `AgentCheckpoint` and `AgentResult`. +4. `continuity` remains the canonical owner for human waiting/attention semantics. Human-dependent blockers or completed long-running delegations surface through Attention rather than a Codex-specific notification path. +5. `epistemics` remains the canonical owner for delegation outcome evidence through `ExperienceRecord` and later reflection, using typed refs to Work, Capability, Artifact and Interaction state. +6. `artifact` remains the place for durable artifact metadata/refs when an agent run produces file, diff, test, log, PR or deployment evidence. Work stores typed refs, not payload copies. +7. `OriginalHumanIntent`, `Goal`, `Work`, `CapabilityRequirement` and `Delegation` must remain canonical semantic records. Agent-specific instruction or prompt text is only a derived adapter artifact with provenance that distinguishes: + - what the human asked; + - what Domovoy inferred; + - what project/runtime context was added; + - how completion will be verified. +8. Execution-window exhaustion is a run lifecycle state, not automatic task failure. The same higher-level delegation may continue through later runs while preserving checkpoint/history. +9. Codex transport/UI wording is adapter-local. Canonical state must stay valid if the coding agent is replaced by another future agent. + +## Ownership + +- `interaction` owns the append-only human request/correction history and original intent traceability. +- `situation` resolves project/repository context and active focus used for delegation context compilation. +- `work` owns durable delegation requests, selected implementation refs, run lifecycle, checkpoint/result refs and verification gating before Work completion. +- `capability` owns executor-neutral capability identity and concrete eligible implementations, including `AGENT` implementations such as Codex. +- `continuity` owns waiting/attention/human-response continuation for delegation progress and genuine human blockers. +- `epistemics` owns experience/reflection evidence derived from delegation outcomes. +- `artifact` owns durable metadata for diffs/logs/checkpoints/results when they must survive outside a transient adapter response. + +## Boundaries + +- `SOFTWARE_ENGINEERING`, `MODIFY_CODE`, `INSPECT_REPOSITORY` and `RUN_TESTS` remain executor-neutral capability semantics. +- `MODIFY_CODE != CODEX`. +- Canonical Work/Delegation does not become a Codex prompt. +- Secret access remains least-privilege through CredentialRef/Secret Resolver. Agent capability availability does not imply credential access or authority. +- A completed agent run does not automatically mark higher-level Work complete. Verification must inspect available evidence. +- Technical failures, execution-window exhaustion and human-dependent blockers are distinct lifecycle states. +- `LLM-free` startup must continue to pass when Codex is unavailable. + +## Consequences + +- Domovoy can delegate suitable software-engineering work without forcing the human to author a giant Codex prompt. +- The first production bridge can target Codex while leaving canonical semantics portable to future scientific, accounting, research or alternate coding agents. +- Long-running agent work can checkpoint and continue across bounded execution windows. +- Human chat remains simple, while operator/debug tooling can inspect the durable delegation/run/checkpoint/result state. diff --git a/cmd/continuity-runtime/attention.go b/cmd/continuity-runtime/attention.go index 39bf5c1..402bfb0 100644 --- a/cmd/continuity-runtime/attention.go +++ b/cmd/continuity-runtime/attention.go @@ -12,6 +12,7 @@ import ( type attentionRepository interface { AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error) + CreateAttentionItem(context.Context, continuity.AttentionItem) (continuity.AttentionItem, error) } func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorIdentityClient, repository attentionRepository) { @@ -61,6 +62,53 @@ func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorId w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(result) }) + mux.HandleFunc("POST /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) { + session, ok := creatorSessionForAttention(w, r, identityClient) + if !ok { + return + } + var request continuity.CreateAttentionRequest + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil { + http.Error(w, "invalid attention request", http.StatusBadRequest) + return + } + if request.OwnerPrincipalID == "" { + request.OwnerPrincipalID = session.PrincipalID + } + if request.OwnerPrincipalID != session.PrincipalID || request.ScopeID == "" || request.SourceKind == "" || request.Level == "" || request.Summary == "" || request.CreatedAtUnixMillis < 1 || request.DueAtUnixMillis < 1 { + http.Error(w, "invalid attention request", http.StatusBadRequest) + return + } + if request.SourceKind == continuity.AttentionSourceAgentDelegation && request.SourceExternalRef == "" { + http.Error(w, "delegation attention requires source external ref", http.StatusBadRequest) + return + } + item := continuity.AttentionItem{ + ID: continuity.AttentionItemID(request.SourceExternalRef), + OwnerPrincipalID: request.OwnerPrincipalID, + ScopeID: request.ScopeID, + SourceKind: request.SourceKind, + SourceProspectiveID: request.SourceProspectiveID, + SourceExternalRef: request.SourceExternalRef, + Level: request.Level, + State: continuity.AttentionPending, + Summary: request.Summary, + DueAtUnixMillis: request.DueAtUnixMillis, + CreatedAtUnixMillis: request.CreatedAtUnixMillis, + Revision: 1, + } + saved, err := repository.CreateAttentionItem(r.Context(), item) + if err != nil { + http.Error(w, "attention unavailable", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(saved) + }) } func creatorSessionForAttention(w http.ResponseWriter, r *http.Request, identityClient creatorIdentityClient) (identity.CreatorSession, bool) { @@ -98,6 +146,15 @@ func humanAttentionState(state continuity.AttentionState) string { } func humanResponseHint(item continuity.AttentionItem) string { + if item.SourceKind == continuity.AttentionSourceAgentDelegation { + if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed { + return "Можно ответить: «Продолжай» или «Нет, нужен другой вариант»." + } + if item.ResponseOutcome != "" { + return "Ответ записан: " + string(item.ResponseOutcome) + } + return "" + } if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed { return "Можно ответить: «Готово», «Отложи на 10 минут» или «Отмени»." } @@ -121,5 +178,8 @@ func humanAttentionSource(kind continuity.AttentionSourceKind) string { if kind == continuity.AttentionSourceProspectiveReminder { return "напоминание" } + if kind == continuity.AttentionSourceAgentDelegation { + return "задача coding-agent" + } return "событие Домового" } diff --git a/cmd/core-runtime/capability_chat.go b/cmd/core-runtime/capability_chat.go index 61f99b2..721e08a 100644 --- a/cmd/core-runtime/capability_chat.go +++ b/cmd/core-runtime/capability_chat.go @@ -19,7 +19,9 @@ import ( "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" + "git.cin.su/m/Assistant/contracts/work" capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain" + workdomain "git.cin.su/m/Assistant/modules/work/domain" "git.cin.su/m/Assistant/platform/ids" ) @@ -33,6 +35,11 @@ type continuityResponseClient struct { client *http.Client } +type continuityAttentionClient struct { + baseURL *url.URL + client *http.Client +} + func newContinuityReminderClient(rawURL string, client *http.Client) (continuityReminderClient, error) { base, err := url.Parse(rawURL) if err != nil || base.Scheme == "" || base.Host == "" { @@ -55,6 +62,17 @@ func newContinuityResponseClient(rawURL string, client *http.Client) (continuity return continuityResponseClient{baseURL: base, client: client}, nil } +func newContinuityAttentionClient(rawURL string, client *http.Client) (continuityAttentionClient, error) { + base, err := url.Parse(rawURL) + if err != nil || base.Scheme == "" || base.Host == "" { + return continuityAttentionClient{}, fmt.Errorf("invalid Continuity Runtime URL") + } + if client == nil { + client = http.DefaultClient + } + return continuityAttentionClient{baseURL: base, client: client}, nil +} + func (c continuityReminderClient) Schedule(ctx context.Context, authorization string, request continuity.ScheduleReminderRequest) (continuity.ScheduleReminderResult, error) { target := *c.baseURL target.Path = "/v1/creator/reminders" @@ -114,6 +132,37 @@ func (c continuityResponseClient) Respond(ctx context.Context, authorization str return result, nil } +func (c continuityAttentionClient) Create(ctx context.Context, authorization string, request continuity.CreateAttentionRequest) (continuity.AttentionItem, error) { + target := *c.baseURL + target.Path = "/v1/creator/attention" + payload, err := json.Marshal(request) + if err != nil { + return continuity.AttentionItem{}, err + } + upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload)) + if err != nil { + return continuity.AttentionItem{}, err + } + upstream.Header.Set("Content-Type", "application/json") + upstream.Header.Set("Authorization", authorization) + response, err := c.client.Do(upstream) + if err != nil { + return continuity.AttentionItem{}, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusCreated { + return continuity.AttentionItem{}, fmt.Errorf("continuity attention status %d", response.StatusCode) + } + var result continuity.AttentionItem + if err := json.NewDecoder(response.Body).Decode(&result); err != nil { + return continuity.AttentionItem{}, err + } + if result.ID == "" || result.SourceKind == "" { + return continuity.AttentionItem{}, fmt.Errorf("invalid attention result") + } + return result, nil +} + func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityRepository, kind capability.CapabilityKind, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (capability.CapabilityResolution, error) { requirementID, err := ids.New() if err != nil { @@ -127,6 +176,7 @@ func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityR OriginalIntentID: capability.OriginalIntentRef(intent.ID), SourceUtteranceID: capability.SourceUtteranceRef(utterance.ID), RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly}, + AllowCanary: allowCanaryForChatCapability(kind), AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1, ObservedAtUnixMillis: now.UnixMilli(), } @@ -144,6 +194,82 @@ func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityR return resolution, err } +func allowCanaryForChatCapability(kind capability.CapabilityKind) bool { + switch kind { + case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject: + return true + default: + return false + } +} + +type createdDelegation struct { + Delegation work.Delegation + Run work.AgentRun +} + +func maybeCreateAgentDelegation(ctx context.Context, workRepository *postgres.WorkRepository, capabilityRepository *postgres.CapabilityRepository, session identity.CreatorSession, conversation interaction.Conversation, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, planning work.PlanningResult, now time.Time) (*createdDelegation, capability.CapabilityResolution, error) { + requirement, ok := primarySoftwareRequirement(planning.Candidate.Requirements) + if !ok { + return nil, capability.CapabilityResolution{}, nil + } + resolution, err := resolveChatCapability(ctx, capabilityRepository, requirement.CapabilityKind, session, intent, utterance, now) + if err != nil { + return nil, resolution, err + } + if len(resolution.Eligible) == 0 || resolution.Eligible[0].Implementation.Kind != capability.ImplementationAgent { + return nil, resolution, capabilitydomain.ErrNoCapabilityImplementation + } + started, err := workdomain.StartDelegation(workdomain.DelegationRequest{ + Goal: planning.Goal, + Plan: planning.Plan, + Candidate: planning.Candidate, + Requirement: requirement, + Implementation: resolution.Eligible[0].Implementation, + ConversationID: work.ConversationRef(conversation.ID), + OriginalIntentText: intent.Text, + ContextSummary: planning.Candidate.Summary, + ContextRefs: []string{"interaction:intent:" + string(intent.ID), "interaction:utterance:" + string(utterance.ID), "work:plan:" + string(planning.Plan.ID)}, + VerificationSummary: "verify through changed files, relevant tests, and explicit run evidence before work completion", + DerivedInstruction: buildSoftwareAgentInstruction(planning, intent, utterance), + StartedAt: now, + }) + if err != nil { + return nil, capability.CapabilityResolution{}, err + } + if err := workRepository.SaveDelegationBundle(ctx, started.Delegation, started.Run); err != nil { + return nil, capability.CapabilityResolution{}, err + } + return &createdDelegation{Delegation: started.Delegation, Run: started.Run}, resolution, nil +} + +func primarySoftwareRequirement(values []work.CapabilityRequirement) (work.CapabilityRequirement, bool) { + for _, requirement := range values { + switch requirement.CapabilityKind { + case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject: + return requirement, true + } + } + return work.CapabilityRequirement{}, false +} + +func buildSoftwareAgentInstruction(planning work.PlanningResult, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance) string { + steps := make([]string, 0, len(planning.Candidate.Steps)) + for _, step := range planning.Candidate.Steps { + if step.Description != "" { + steps = append(steps, step.Description) + } + } + return strings.TrimSpace(strings.Join([]string{ + "Human asked: " + strings.TrimSpace(intent.Text), + "Original utterance: " + strings.TrimSpace(utterance.Content), + "Desired outcome: " + planning.Goal.DesiredOutcome, + "Plan summary: " + planning.Candidate.Summary, + "Verification: run relevant tests and keep explicit evidence before claiming completion.", + "Steps: " + strings.Join(steps, " | "), + }, "\n")) +} + func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) (capability.CapabilityUsageObservationID, error) { id, err := ids.New() if err != nil { diff --git a/cmd/core-runtime/capability_chat_test.go b/cmd/core-runtime/capability_chat_test.go index deb0168..5eec4ef 100644 --- a/cmd/core-runtime/capability_chat_test.go +++ b/cmd/core-runtime/capability_chat_test.go @@ -9,9 +9,11 @@ import ( "testing" "time" + "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" + "git.cin.su/m/Assistant/contracts/work" ) func TestBuildReminderRequestParsesTomorrowAtTen(t *testing.T) { @@ -72,3 +74,29 @@ func TestReminderSummaryRemovesSchedulingWords(t *testing.T) { t.Fatalf("summary = %q", summary) } } + +func TestBuildSoftwareAgentInstructionKeepsHumanIntentSeparateFromDerivedContext(t *testing.T) { + instruction := buildSoftwareAgentInstruction( + work.PlanningResult{ + Goal: work.Goal{DesiredOutcome: "fix chat wording"}, + Candidate: work.PlanCandidate{Summary: "accepted deterministic software change plan", Steps: []work.PlanStep{{Description: "Inspect chat surface"}, {Description: "Change copy"}, {Description: "Run tests"}}}, + }, + interaction.OriginalHumanIntent{ID: "intent-1", Text: "Исправь текст в чате Assistant."}, + interaction.HumanUtterance{ID: "utterance-1", Content: "Исправь текст в чате Assistant."}, + ) + if !strings.Contains(instruction, "Human asked: Исправь текст в чате Assistant.") { + t.Fatalf("instruction must preserve human intent: %q", instruction) + } + if !strings.Contains(instruction, "Verification: run relevant tests") || !strings.Contains(instruction, "Plan summary: accepted deterministic software change plan") { + t.Fatalf("instruction must include derived execution context: %q", instruction) + } +} + +func TestAllowCanaryForChatCapabilityIsLimitedToSoftwareEngineeringPath(t *testing.T) { + if !allowCanaryForChatCapability(capability.CapabilityKindModifyCode) || !allowCanaryForChatCapability(capability.CapabilityKindRunTests) || !allowCanaryForChatCapability(capability.CapabilityKindInspectProject) { + t.Fatal("software-engineering capabilities must allow canary implementations") + } + if allowCanaryForChatCapability(capability.CapabilityKindScheduleReminder) { + t.Fatal("non software-engineering capability must not implicitly allow canary") + } +} diff --git a/cmd/core-runtime/learning.go b/cmd/core-runtime/learning.go index 0c4801f..eb54dc0 100644 --- a/cmd/core-runtime/learning.go +++ b/cmd/core-runtime/learning.go @@ -3,6 +3,7 @@ package main import ( "context" "log" + "strconv" "strings" "time" @@ -68,6 +69,20 @@ type capabilityLearningInput struct { At time.Time } +type delegationLearningInput struct { + Intent interaction.OriginalHumanIntent + Utterance interaction.HumanUtterance + DelegationID string + RunID string + ImplementationID capability.CapabilityImplementationID + OutcomeRef string + VerificationRefs []epistemics.EvidenceRef + Verified bool + ExecutionWindows int + HumanBlocked bool + At time.Time +} + func (r learningRecorder) recordReminderResponse(ctx context.Context, input reminderResponseLearningInput) { if r.epistemics == nil || r.capability == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.Result.Response.ID == "" { return @@ -200,6 +215,42 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil r.saveExperienceAndReflection(ctx, experience) } +func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) { + if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.ImplementationID == "" { + return + } + actual := epistemics.OutcomeSuccess + confidence := int64(950) + if !input.Verified { + actual = epistemics.OutcomeFailed + confidence = 850 + } + detailParts := []string{"delegation outcome recorded"} + if input.ExecutionWindows > 1 { + detailParts = append(detailParts, "execution-window-continuations="+strconv.Itoa(input.ExecutionWindows-1)) + } + if input.HumanBlocked { + detailParts = append(detailParts, "human-blocker-occurred") + } + experience := epistemics.ExperienceRecord{ + SourceKind: epistemics.ExperienceSourceCapability, + OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID), + SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID), + ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID), + HelpMethod: epistemics.HelpMethodCapabilityExecution, + HelpMethodRef: "work:agent-delegation-v1", + CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID), + ExpectedOutcome: epistemics.OutcomeSuccess, + ActualOutcome: actual, + OutcomeRef: input.OutcomeRef, + OutcomeDetail: strings.Join(detailParts, "; "), + VerificationEvidence: input.VerificationRefs, + ConfidenceMillis: confidence, + RecordedAtUnixMillis: input.At.UnixMilli(), + } + r.saveExperienceAndReflection(ctx, experience) +} + func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) { value, err := epistemicsdomain.NewExperienceRecord(value) if err != nil { diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index dfa5364..c12e813 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -107,6 +107,10 @@ func main() { if err != nil { log.Fatal(err) } + attentionClient, err := newContinuityAttentionClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second}) + if err != nil { + log.Fatal(err) + } learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository} mux := http.NewServeMux() authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) { @@ -327,6 +331,9 @@ func main() { planningReply = responseResult.Reply needsCognition = false learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now}) + if resumed, err := continueDelegationFromHumanResponse(r.Context(), workRepository, responseResult.Response, now); err == nil && resumed { + planningReply = "Принял. Продолжаю ту же задачу coding-agent без потери контекста." + } } else if responseErr == nil && looksLikeUnsupportedContinuation(utterance.Content, responseResult) { learning.recordFastPathDecline(r.Context(), fastPathDeclineLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, Reason: declineReasonForResponse(responseResult), At: now}) } @@ -365,6 +372,8 @@ func main() { var capabilityStatus capability.CapabilityResolutionStatus var capabilityKind capability.CapabilityKind var capabilityGapID capability.CapabilityGapID + var delegationID work.DelegationID + var delegationState work.DelegationState var reminder *continuity.ProspectiveItem if helpPath.Kind == work.HelpPathDirectCapability && len(helpPath.SuggestedCapabilityKinds) > 0 { capabilityKind = helpPath.SuggestedCapabilityKinds[0] @@ -423,6 +432,18 @@ func main() { planningReply = planning.Reply workPlanned = true needsCognition = false + if created, resolution, createErr := maybeCreateAgentDelegation(r.Context(), workRepository, capabilityRepository, session, conversation, intent, utterance, planning, now); createErr == nil && created != nil { + delegationID = created.Delegation.ID + delegationState = created.Delegation.State + capabilityKind = created.Delegation.CapabilityKind + capabilityStatus = resolution.Status + planningReply = "Понял задачу. Я собрал work item, выбрал coding-agent в CANARY-режиме и создал delegation. Буду вести её как отдельный run с checkpoint/resume, без потери исходного human intent." + } else if resolution.Gap != nil { + capabilityKind = resolution.Requirement.CapabilityKind + capabilityStatus = resolution.Status + capabilityGapID = resolution.Gap.ID + planningReply = planningReply + " Для coding delegation подходящий agent implementation сейчас недоступен, поэтому я сохранил план без ложного запуска." + } } response := interaction.ChatMessageResponse{ ConversationID: conversation.ID, @@ -440,6 +461,8 @@ func main() { CapabilityStatus: capabilityStatus, CapabilityKind: capabilityKind, CapabilityGapID: capabilityGapID, + DelegationID: string(delegationID), + DelegationState: string(delegationState), Reminder: reminder, } w.Header().Set("Content-Type", "application/json") @@ -447,6 +470,210 @@ func main() { w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(response) }) + mux.HandleFunc("GET /v1/creator/delegations/{id}", func(w http.ResponseWriter, r *http.Request) { + if _, ok := authenticateCreator(w, r); !ok { + return + } + value, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id"))) + if err != nil { + http.Error(w, "delegation not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(value) + }) + mux.HandleFunc("GET /v1/creator/delegations/{id}/latest-checkpoint", func(w http.ResponseWriter, r *http.Request) { + if _, ok := authenticateCreator(w, r); !ok { + return + } + value, err := workRepository.LatestAgentCheckpoint(r.Context(), work.DelegationID(r.PathValue("id"))) + if err != nil { + http.Error(w, "checkpoint not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(value) + }) + mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/checkpoints", func(w http.ResponseWriter, r *http.Request) { + session, ok := authenticateCreator(w, r) + if !ok { + return + } + var request struct { + BlockerClass work.AgentBlockerClass + CompletedSummary string + RemainingSummary string + TestSummary string + BranchName string + CommitHash string + NextStep string + ArtifactRefs []work.EvidenceRef + RecordedAtUnixMillis int64 + } + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil { + http.Error(w, "invalid checkpoint", http.StatusBadRequest) + return + } + run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run"))) + if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) { + http.Error(w, "run not found", http.StatusNotFound) + return + } + updatedRun, checkpoint, err := workdomain.CheckpointRun(run, request.BlockerClass, request.CompletedSummary, request.RemainingSummary, request.TestSummary, request.BranchName, request.CommitHash, request.NextStep, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis)) + if err != nil { + http.Error(w, "invalid checkpoint", http.StatusBadRequest) + return + } + if err := workRepository.SaveAgentCheckpoint(r.Context(), updatedRun, checkpoint); err != nil { + http.Error(w, "save checkpoint", http.StatusInternalServerError) + return + } + if request.BlockerClass == work.AgentBlockerHumanDecisionRequired { + delegation, err := workRepository.LoadDelegation(r.Context(), run.DelegationID) + if err == nil { + _, _ = attentionClient.Create(r.Context(), r.Header.Get("Authorization"), continuity.CreateAttentionRequest{ + OwnerPrincipalID: session.PrincipalID, + ScopeID: authority.ScopeID("domovoy-global-scope"), + SourceKind: continuity.AttentionSourceAgentDelegation, + SourceExternalRef: delegationAttentionRef(delegation.ID), + Level: continuity.AttentionActionRequired, + Summary: "Нужен твой выбор по задаче coding-agent: " + firstNonEmpty(request.RemainingSummary, delegation.DesiredOutcome), + DueAtUnixMillis: checkpoint.RecordedAtUnixMillis, + CreatedAtUnixMillis: checkpoint.RecordedAtUnixMillis, + }) + } + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(checkpoint) + }) + mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/results", func(w http.ResponseWriter, r *http.Request) { + if _, ok := authenticateCreator(w, r); !ok { + return + } + var request struct { + Summary string + OutcomeRef string + VerificationSummary string + ArtifactRefs []work.EvidenceRef + RecordedAtUnixMillis int64 + } + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil { + http.Error(w, "invalid result", http.StatusBadRequest) + return + } + run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run"))) + if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) { + http.Error(w, "run not found", http.StatusNotFound) + return + } + updatedRun, result, err := workdomain.CompleteRun(run, request.Summary, request.OutcomeRef, request.VerificationSummary, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis)) + if err != nil { + http.Error(w, "invalid result", http.StatusBadRequest) + return + } + if err := workRepository.SaveAgentResult(r.Context(), updatedRun, result); err != nil { + http.Error(w, "save result", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(result) + }) + mux.HandleFunc("POST /v1/creator/delegations/{id}/resume", func(w http.ResponseWriter, r *http.Request) { + if _, ok := authenticateCreator(w, r); !ok { + return + } + delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id"))) + if err != nil { + http.Error(w, "delegation not found", http.StatusNotFound) + return + } + run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID) + if err != nil { + http.Error(w, "run not found", http.StatusNotFound) + return + } + checkpoint, err := workRepository.LatestAgentCheckpoint(r.Context(), delegation.ID) + if err != nil { + http.Error(w, "checkpoint not found", http.StatusNotFound) + return + } + updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, time.Now()) + if err != nil { + http.Error(w, "resume not allowed", http.StatusConflict) + return + } + if err := workRepository.SaveResumedRun(r.Context(), updatedDelegation, resumedRun); err != nil { + http.Error(w, "save resumed run", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(resumedRun) + }) + mux.HandleFunc("POST /v1/creator/delegations/{id}/verify", func(w http.ResponseWriter, r *http.Request) { + if _, ok := authenticateCreator(w, r); !ok { + return + } + var request struct { + Verified bool + Summary string + EvidenceRefs []work.EvidenceRef + RecordedAtUnixMillis int64 + } + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil || request.RecordedAtUnixMillis < 1 { + http.Error(w, "invalid verification", http.StatusBadRequest) + return + } + delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id"))) + if err != nil { + http.Error(w, "delegation not found", http.StatusNotFound) + return + } + run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID) + if err != nil { + http.Error(w, "run not found", http.StatusNotFound) + return + } + result, err := workRepository.LatestAgentResult(r.Context(), delegation.ID) + if err != nil { + http.Error(w, "result not found", http.StatusNotFound) + return + } + updatedDelegation, verification, err := workdomain.VerifyDelegationResult(delegation, run, result, request.Verified, request.Summary, request.EvidenceRefs, time.UnixMilli(request.RecordedAtUnixMillis)) + if err != nil { + http.Error(w, "invalid verification", http.StatusBadRequest) + return + } + if err := workRepository.SaveDelegationVerification(r.Context(), updatedDelegation, verification); err != nil { + http.Error(w, "save verification", http.StatusInternalServerError) + return + } + runCount, _ := workRepository.CountAgentRuns(r.Context(), delegation.ID) + learning.recordDelegationOutcome(r.Context(), delegationLearningInput{ + Intent: interaction.OriginalHumanIntent{ID: interaction.OriginalHumanIntentID(delegation.SourceIntentID), Text: delegation.DesiredOutcome}, + Utterance: interaction.HumanUtterance{ID: interaction.HumanUtteranceID(delegation.SourceUtteranceID), Content: delegation.DesiredOutcome}, + DelegationID: string(delegation.ID), + RunID: string(run.ID), + ImplementationID: run.ImplementationID, + OutcomeRef: result.OutcomeRef, + VerificationRefs: evidenceRefsToEpistemics(request.EvidenceRefs), + Verified: request.Verified, + ExecutionWindows: runCount, + HumanBlocked: false, + At: time.UnixMilli(request.RecordedAtUnixMillis), + }) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(verification) + }) mux.HandleFunc("GET /v1/principal-bindings/{actor}/{principal}", func(w http.ResponseWriter, r *http.Request) { binding, err := identityRepository.LoadBinding(r.Context(), identity.ActorID(r.PathValue("actor")), identity.PrincipalID(r.PathValue("principal"))) if err != nil { @@ -859,3 +1086,54 @@ func humanCapabilityList(kinds []capability.CapabilityKind) string { } return strings.Join(labels, ", ") } + +func delegationAttentionRef(id work.DelegationID) string { + return "work:delegation:" + string(id) +} + +func continueDelegationFromHumanResponse(ctx context.Context, repository *postgres.WorkRepository, response continuity.HumanResponse, now time.Time) (bool, error) { + if response.Operation != continuity.ResponseContinue || !strings.HasPrefix(response.TargetExternalRef, "work:delegation:") { + return false, nil + } + delegationID := work.DelegationID(strings.TrimPrefix(response.TargetExternalRef, "work:delegation:")) + delegation, err := repository.LoadDelegation(ctx, delegationID) + if err != nil { + return false, err + } + run, err := repository.LoadAgentRun(ctx, delegation.LatestRunID) + if err != nil { + return false, err + } + checkpoint, err := repository.LatestAgentCheckpoint(ctx, delegationID) + if err != nil { + return false, err + } + updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, now) + if err != nil { + return false, err + } + if err := repository.SaveResumedRun(ctx, updatedDelegation, resumedRun); err != nil { + return false, err + } + return true, nil +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + +func evidenceRefsToEpistemics(values []work.EvidenceRef) []epistemics.EvidenceRef { + result := make([]epistemics.EvidenceRef, 0, len(values)) + for _, value := range values { + if strings.TrimSpace(string(value)) == "" { + continue + } + result = append(result, epistemics.EvidenceRef(value)) + } + return result +} diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index 70bcb97..0915183 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -154,6 +154,8 @@ type browserChatResponse struct { WorkPlanned bool `json:"work_planned"` CapabilityStatus string `json:"capability_status"` CapabilityKind string `json:"capability_kind"` + DelegationID string `json:"delegation_id"` + DelegationState string `json:"delegation_state"` ReminderScheduled bool `json:"reminder_scheduled"` } @@ -353,7 +355,22 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") - _ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned, CapabilityStatus: string(saved.CapabilityStatus), CapabilityKind: string(saved.CapabilityKind), ReminderScheduled: saved.Reminder != nil}) + _ = json.NewEncoder(w).Encode(browserChatResponse{ + ConversationID: string(saved.ConversationID), + UtteranceID: string(saved.UtteranceID), + SpeechAct: string(saved.SpeechAct), + ReplyText: reply, + Cognition: cognition, + ActiveFocus: saved.ActiveProjectName, + ProjectProvenance: string(saved.ProjectProvenance), + Clarification: string(saved.Clarification), + WorkPlanned: saved.WorkPlanned, + CapabilityStatus: string(saved.CapabilityStatus), + CapabilityKind: string(saved.CapabilityKind), + DelegationID: saved.DelegationID, + DelegationState: saved.DelegationState, + ReminderScheduled: saved.Reminder != nil, + }) } func asksAttention(message string) bool { diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js index 1fb07d6..70c6dd1 100644 --- a/cmd/creator-web/static/assets/chat.js +++ b/cmd/creator-web/static/assets/chat.js @@ -62,11 +62,31 @@ async function api(path, options = {}) { function humanMeta(result) { if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам."; + if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`; if (result.capability_status && result.capability_status !== "AVAILABLE") return "Возможность пока не подключена."; if (result.work_planned) return "Я разложил это как работу."; return ""; } +function humanDelegationState(state) { + switch (state) { + case "RUNNING": + return "работа запущена"; + case "WAITING": + return "ожидаю следующий шаг"; + case "BLOCKED": + return "нужно решение человека"; + case "VERIFYING": + return "результат проверяется"; + case "COMPLETED": + return "работа завершена"; + case "FAILED": + return "run завершился ошибкой"; + default: + return state.toLowerCase(); + } +} + function renderHistory() { if (!historyList) return; historyList.replaceChildren(); diff --git a/contracts/continuity/types.go b/contracts/continuity/types.go index b33c12b..c6c0932 100644 --- a/contracts/continuity/types.go +++ b/contracts/continuity/types.go @@ -58,6 +58,7 @@ type AttentionSourceKind string const ( AttentionSourceProspectiveReminder AttentionSourceKind = "PROSPECTIVE_REMINDER" + AttentionSourceAgentDelegation AttentionSourceKind = "AGENT_DELEGATION" ) // ProspectiveItem is canonical future relevance. TriggerAtUnixMillis is an @@ -99,6 +100,7 @@ type AttentionItem struct { ScopeID authority.ScopeID SourceKind AttentionSourceKind SourceProspectiveID ProspectiveItemID + SourceExternalRef string Level AttentionLevel State AttentionState Summary string @@ -177,6 +179,8 @@ const ( ResponseOutcomeCancelled ResponseOutcome = "CANCELLED" ResponseOutcomeDismissed ResponseOutcome = "DISMISSED" ResponseOutcomeClarificationNeeded ResponseOutcome = "CLARIFICATION_NEEDED" + ResponseOutcomeContinued ResponseOutcome = "CONTINUED" + ResponseOutcomeRejected ResponseOutcome = "REJECTED" ResponseOutcomeNoop ResponseOutcome = "NOOP" ) @@ -213,6 +217,18 @@ type HumanResponseResult struct { Reply string } +type CreateAttentionRequest struct { + OwnerPrincipalID identity.PrincipalID + ScopeID authority.ScopeID + SourceKind AttentionSourceKind + SourceProspectiveID ProspectiveItemID + SourceExternalRef string + Level AttentionLevel + Summary string + DueAtUnixMillis int64 + CreatedAtUnixMillis int64 +} + // SatisfyDeliveredSubject is the complete serializable operation subject. It // is distinct from a Work TaskTarget, which remains the live justification. type SatisfyDeliveredSubject struct { diff --git a/contracts/interaction/types.go b/contracts/interaction/types.go index cd0a331..a272a92 100644 --- a/contracts/interaction/types.go +++ b/contracts/interaction/types.go @@ -136,5 +136,7 @@ type ChatMessageResponse struct { CapabilityStatus capability.CapabilityResolutionStatus CapabilityKind capability.CapabilityKind CapabilityGapID capability.CapabilityGapID + DelegationID string + DelegationState string Reminder *continuity.ProspectiveItem } diff --git a/contracts/work/types.go b/contracts/work/types.go index 1b5f1c2..d6a287f 100644 --- a/contracts/work/types.go +++ b/contracts/work/types.go @@ -16,6 +16,11 @@ type WorkPlanID ids.ID type PlanStepID ids.ID type CapabilityRequirementID ids.ID type DelegationCandidateID ids.ID +type DelegationID ids.ID +type AgentRunID ids.ID +type AgentCheckpointID ids.ID +type AgentResultID ids.ID +type DelegationVerificationID ids.ID type TaskRevision uint64 type GoalRevision uint64 type PlanRevision uint64 @@ -25,6 +30,11 @@ type PlanStepKind string type CapabilityRequirementState string type DelegationCandidateState string type DelegationKind string +type DelegationState string +type AgentRunState string +type AgentBlockerClass string +type AgentExecutionProtocol string +type EvidenceRef string type SourceIntentRef string type SourceUtteranceRef string type ConversationRef string @@ -78,6 +88,35 @@ const ( DelegationKindHuman DelegationKind = "HUMAN" DelegationKindUnavailable DelegationKind = "UNAVAILABLE" + DelegationPending DelegationState = "PENDING" + DelegationRunning DelegationState = "RUNNING" + DelegationWaiting DelegationState = "WAITING" + DelegationBlocked DelegationState = "BLOCKED" + DelegationVerifying DelegationState = "VERIFYING" + DelegationCompleted DelegationState = "COMPLETED" + DelegationFailed DelegationState = "FAILED" + DelegationCancelled DelegationState = "CANCELLED" + + AgentRunStarted AgentRunState = "STARTED" + AgentRunRunning AgentRunState = "RUNNING" + AgentRunWaiting AgentRunState = "WAITING" + AgentRunCheckpointed AgentRunState = "CHECKPOINTED" + AgentRunExecutionWindowExhausted AgentRunState = "EXECUTION_WINDOW_EXHAUSTED" + AgentRunBlocked AgentRunState = "BLOCKED" + AgentRunFailed AgentRunState = "FAILED" + AgentRunCompleted AgentRunState = "COMPLETED" + AgentRunCancelled AgentRunState = "CANCELLED" + + AgentBlockerNone AgentBlockerClass = "NONE" + AgentBlockerHumanDecisionRequired AgentBlockerClass = "HUMAN_DECISION_REQUIRED" + AgentBlockerMissingExternalCredential AgentBlockerClass = "MISSING_EXTERNAL_CREDENTIAL" + AgentBlockerArchitectureDecision AgentBlockerClass = "ARCHITECTURE_DECISION_REQUIRED" + AgentBlockerSecurityConflict AgentBlockerClass = "SECURITY_CONFLICT" + AgentBlockerTechnicalFailure AgentBlockerClass = "TECHNICAL_FAILURE" + AgentBlockerExecutionWindow AgentBlockerClass = "EXECUTION_WINDOW_EXHAUSTED" + + AgentProtocolCodexBridgeV1 AgentExecutionProtocol = "CODEX_BRIDGE_V1" + HelpPathAnswer HelpPathKind = "ANSWER" HelpPathCognition HelpPathKind = "COGNITION" HelpPathDirectCapability HelpPathKind = "DIRECT_CAPABILITY" @@ -168,6 +207,87 @@ type WorkPlan struct { RecordedAtUnixMillis int64 } +type Delegation struct { + ID DelegationID + GoalID GoalID + PlanID WorkPlanID + PlanCandidateID PlanCandidateID + CapabilityRequirementID CapabilityRequirementID + CapabilityKind capability.CapabilityKind + SelectedImplementationID capability.CapabilityImplementationID + SelectedImplementationRev int64 + SelectedImplementationKind capability.ImplementationKind + Protocol AgentExecutionProtocol + SourceIntentID SourceIntentRef + SourceUtteranceID SourceUtteranceRef + ConversationID ConversationRef + ProjectID ProjectRef + ProjectDisplayName string + DesiredOutcome string + ConstraintSummary string + VerificationSummary string + ContextSummary string + ContextRefs []string + State DelegationState + LatestRunID AgentRunID + LatestResultID AgentResultID + CreatedAtUnixMillis int64 + UpdatedAtUnixMillis int64 +} + +type AgentRun struct { + ID AgentRunID + DelegationID DelegationID + ImplementationID capability.CapabilityImplementationID + ImplementationRevision int64 + Protocol AgentExecutionProtocol + State AgentRunState + DerivedInstructionSummary string + StartedAtUnixMillis int64 + UpdatedAtUnixMillis int64 + CompletedAtUnixMillis int64 +} + +type AgentCheckpoint struct { + ID AgentCheckpointID + DelegationID DelegationID + RunID AgentRunID + State AgentRunState + BlockerClass AgentBlockerClass + CompletedSummary string + RemainingSummary string + TestSummary string + BranchName string + CommitHash string + NextStep string + ArtifactRefs []EvidenceRef + RecordedAtUnixMillis int64 +} + +type AgentResult struct { + ID AgentResultID + DelegationID DelegationID + RunID AgentRunID + State AgentRunState + Summary string + OutcomeRef string + VerificationRequired bool + VerificationSummary string + ArtifactRefs []EvidenceRef + RecordedAtUnixMillis int64 +} + +type DelegationVerification struct { + ID DelegationVerificationID + DelegationID DelegationID + RunID AgentRunID + ResultID AgentResultID + Verified bool + Summary string + EvidenceRefs []EvidenceRef + RecordedAtUnixMillis int64 +} + type PlanningInput struct { ConversationID ConversationRef UtteranceID SourceUtteranceRef diff --git a/database/migrations/024_work_agent_delegation_foundation.sql b/database/migrations/024_work_agent_delegation_foundation.sql new file mode 100644 index 0000000..d59c7a7 --- /dev/null +++ b/database/migrations/024_work_agent_delegation_foundation.sql @@ -0,0 +1,145 @@ +BEGIN; + +SET ROLE work_role; + +CREATE TABLE IF NOT EXISTS work.delegations ( + delegation_id TEXT PRIMARY KEY, + goal_id TEXT NOT NULL, + work_plan_id TEXT NOT NULL, + plan_candidate_id TEXT NOT NULL, + capability_requirement_id TEXT NOT NULL, + capability_kind TEXT NOT NULL, + selected_implementation_id TEXT NOT NULL, + selected_implementation_revision BIGINT NOT NULL, + selected_implementation_kind TEXT NOT NULL, + protocol TEXT NOT NULL, + source_intent_id TEXT NOT NULL, + source_utterance_id TEXT NOT NULL, + conversation_id TEXT NOT NULL DEFAULT '', + project_id TEXT NOT NULL DEFAULT '', + project_display_name TEXT NOT NULL DEFAULT '', + desired_outcome TEXT NOT NULL, + constraint_summary TEXT NOT NULL DEFAULT '', + verification_summary TEXT NOT NULL DEFAULT '', + context_summary TEXT NOT NULL DEFAULT '', + context_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + state TEXT NOT NULL, + latest_run_id TEXT NOT NULL DEFAULT '', + latest_result_id TEXT NOT NULL DEFAULT '', + created_at_unix_millis BIGINT NOT NULL, + updated_at_unix_millis BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (delegation_id <> ''), + CHECK (goal_id <> ''), + CHECK (work_plan_id <> ''), + CHECK (plan_candidate_id <> ''), + CHECK (capability_requirement_id <> ''), + CHECK (capability_kind <> ''), + CHECK (selected_implementation_id <> ''), + CHECK (selected_implementation_revision > 0), + CHECK (selected_implementation_kind = 'AGENT'), + CHECK (protocol IN ('CODEX_BRIDGE_V1')), + CHECK (source_intent_id <> ''), + CHECK (source_utterance_id <> ''), + CHECK (desired_outcome <> ''), + CHECK (state IN ('PENDING','RUNNING','WAITING','BLOCKED','VERIFYING','COMPLETED','FAILED','CANCELLED')), + CHECK (created_at_unix_millis > 0), + CHECK (updated_at_unix_millis >= created_at_unix_millis) +); + +CREATE TABLE IF NOT EXISTS work.agent_runs ( + run_id TEXT PRIMARY KEY, + delegation_id TEXT NOT NULL, + implementation_id TEXT NOT NULL, + implementation_revision BIGINT NOT NULL, + protocol TEXT NOT NULL, + state TEXT NOT NULL, + derived_instruction_summary TEXT NOT NULL DEFAULT '', + started_at_unix_millis BIGINT NOT NULL, + updated_at_unix_millis BIGINT NOT NULL, + completed_at_unix_millis BIGINT NOT NULL DEFAULT 0, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (run_id <> ''), + CHECK (delegation_id <> ''), + CHECK (implementation_id <> ''), + CHECK (implementation_revision > 0), + CHECK (protocol IN ('CODEX_BRIDGE_V1')), + CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')), + CHECK (started_at_unix_millis > 0), + CHECK (updated_at_unix_millis >= started_at_unix_millis), + CHECK (completed_at_unix_millis >= 0) +); + +CREATE TABLE IF NOT EXISTS work.agent_checkpoints ( + checkpoint_id TEXT PRIMARY KEY, + delegation_id TEXT NOT NULL, + run_id TEXT NOT NULL, + state TEXT NOT NULL, + blocker_class TEXT NOT NULL, + completed_summary TEXT NOT NULL DEFAULT '', + remaining_summary TEXT NOT NULL DEFAULT '', + test_summary TEXT NOT NULL DEFAULT '', + branch_name TEXT NOT NULL DEFAULT '', + commit_hash TEXT NOT NULL DEFAULT '', + next_step TEXT NOT NULL DEFAULT '', + artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + recorded_at_unix_millis BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (checkpoint_id <> ''), + CHECK (delegation_id <> ''), + CHECK (run_id <> ''), + CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')), + CHECK (blocker_class IN ('NONE','HUMAN_DECISION_REQUIRED','MISSING_EXTERNAL_CREDENTIAL','ARCHITECTURE_DECISION_REQUIRED','SECURITY_CONFLICT','TECHNICAL_FAILURE','EXECUTION_WINDOW_EXHAUSTED')), + CHECK (recorded_at_unix_millis > 0) +); + +CREATE TABLE IF NOT EXISTS work.agent_results ( + result_id TEXT PRIMARY KEY, + delegation_id TEXT NOT NULL, + run_id TEXT NOT NULL, + state TEXT NOT NULL, + summary TEXT NOT NULL, + outcome_ref TEXT NOT NULL DEFAULT '', + verification_required BOOLEAN NOT NULL, + verification_summary TEXT NOT NULL DEFAULT '', + artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + recorded_at_unix_millis BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (result_id <> ''), + CHECK (delegation_id <> ''), + CHECK (run_id <> ''), + CHECK (state IN ('STARTED','RUNNING','WAITING','CHECKPOINTED','EXECUTION_WINDOW_EXHAUSTED','BLOCKED','FAILED','COMPLETED','CANCELLED')), + CHECK (summary <> ''), + CHECK (recorded_at_unix_millis > 0) +); + +RESET ROLE; + +SET ROLE capability_role; + +INSERT INTO capability.capability_descriptors(capability_id, capability_kind, display_name, description, revision, recorded_at_unix_millis) +VALUES + ('modify-code', 'MODIFY_CODE', 'Code modification', 'Executor-neutral software engineering capability that changes repository code inside an authorized scope.', 1, 1), + ('run-tests', 'RUN_TESTS', 'Run tests', 'Executor-neutral software engineering capability that runs repository validation and test commands.', 1, 1), + ('inspect-project', 'INSPECT_PROJECT', 'Inspect project', 'Executor-neutral software engineering capability that inspects repository structure and relevant files.', 1, 1) +ON CONFLICT (capability_id) DO NOTHING; + +INSERT INTO capability.capability_implementations( + implementation_id, capability_id, capability_kind, owner_principal_id, revision, implementation_kind, target_ref, + credential_ref, qualification, activation, health, authority_policy_ref, data_constraints, + cost_score_millis, latency_score_millis, reliability_millis, observed_at_unix_millis) +VALUES + ('codex-agent-modify-code-v1', 'modify-code', 'MODIFY_CODE', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/modify-code', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 80, 820, 1), + ('codex-agent-run-tests-v1', 'run-tests', 'RUN_TESTS', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/run-tests', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 120, 860, 1), + ('codex-agent-inspect-project-v1', 'inspect-project', 'INSPECT_PROJECT', 'domovoy-creator-principal', 1, 'AGENT', 'codex://assistant/inspect-project', '', 'VALIDATED', 'CANARY', 'AVAILABLE', 'creator-directive-v1', ARRAY['LOCAL_ONLY','NO_CREDENTIAL_FORWARD'], 5, 40, 900, 1) +ON CONFLICT DO NOTHING; + +INSERT INTO capability.capability_implementation_heads(implementation_id, revision, capability_kind, owner_principal_id) +VALUES + ('codex-agent-modify-code-v1', 1, 'MODIFY_CODE', 'domovoy-creator-principal'), + ('codex-agent-run-tests-v1', 1, 'RUN_TESTS', 'domovoy-creator-principal'), + ('codex-agent-inspect-project-v1', 1, 'INSPECT_PROJECT', 'domovoy-creator-principal') +ON CONFLICT (implementation_id) DO NOTHING; + +RESET ROLE; +COMMIT; diff --git a/database/migrations/025_delegation_attention_and_verification.sql b/database/migrations/025_delegation_attention_and_verification.sql new file mode 100644 index 0000000..a0be55b --- /dev/null +++ b/database/migrations/025_delegation_attention_and_verification.sql @@ -0,0 +1,60 @@ +BEGIN; + +SET ROLE continuity_role; + +ALTER TABLE continuity.attention_items + ADD COLUMN IF NOT EXISTS source_external_ref TEXT NOT NULL DEFAULT ''; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'attention_items_source_kind_check_v2' + ) THEN + ALTER TABLE continuity.attention_items DROP CONSTRAINT IF EXISTS attention_items_source_kind_check; + ALTER TABLE continuity.attention_items + ADD CONSTRAINT attention_items_source_kind_check_v2 + CHECK (source_kind IN ('PROSPECTIVE_REMINDER','AGENT_DELEGATION')); + END IF; +END $$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'human_responses_outcome_check_v2' + ) THEN + ALTER TABLE continuity.human_responses DROP CONSTRAINT IF EXISTS human_responses_outcome_check; + ALTER TABLE continuity.human_responses + ADD CONSTRAINT human_responses_outcome_check_v2 + CHECK (outcome IN ('USER_REPORTED_COMPLETE','SNOOZED','CANCELLED','DISMISSED','CLARIFICATION_NEEDED','CONTINUED','REJECTED','NOOP')); + END IF; +END $$; + +CREATE UNIQUE INDEX IF NOT EXISTS attention_items_source_external_ref_unique + ON continuity.attention_items(source_kind, source_external_ref) + WHERE source_external_ref <> ''; + +RESET ROLE; + +SET ROLE work_role; + +CREATE TABLE IF NOT EXISTS work.delegation_verifications ( + verification_id TEXT PRIMARY KEY, + delegation_id TEXT NOT NULL, + run_id TEXT NOT NULL, + result_id TEXT NOT NULL, + verified BOOLEAN NOT NULL, + summary TEXT NOT NULL, + evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + recorded_at_unix_millis BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (delegation_id <> ''), + CHECK (run_id <> ''), + CHECK (result_id <> ''), + CHECK (summary <> ''), + CHECK (recorded_at_unix_millis > 0) +); + +RESET ROLE; +COMMIT; diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 39f90fa..0dd88dd 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000068-runtime-experience-reflection-loop.json", - "task_id": "DEV-000068", + "task_file": "dev/tasks/DEV-000069-general-agent-delegation-codex-bridge.json", + "task_id": "DEV-000069", "task_revision": 1 } diff --git a/dev/tasks/DEV-000069-general-agent-delegation-codex-bridge.json b/dev/tasks/DEV-000069-general-agent-delegation-codex-bridge.json new file mode 100644 index 0000000..d95854d --- /dev/null +++ b/dev/tasks/DEV-000069-general-agent-delegation-codex-bridge.json @@ -0,0 +1,163 @@ +{ + "task_id": "DEV-000069", + "task_revision": 1, + "architecture_revision": "ADR-0017", + "base_repo_revision": "d870e1361d6dcef3866d7339495d83b9756949c4", + "risk_tier": "E", + "intent": "Introduce a provider-neutral external intelligent agent delegation foundation and the first Codex bridge implementation for software-engineering work without making Codex a canonical subsystem.", + "requirement_ids": [ + "ADR-0017", + "ADR-0012", + "ADR-0013", + "ADR-0016", + "INV-OWN-001", + "INV-EPI-001", + "INV-TOPO-001", + "General external agent representation", + "Durable delegation and resumable runs", + "Codex bridge as first AGENT implementation", + "Execution-window continuation", + "Delegation experience evidence" + ], + "allowed_change_scope": { + "modules": [ + "work", + "capability", + "continuity", + "epistemics", + "interaction", + "situation", + "artifact" + ], + "paths": [ + "contracts/work/**", + "contracts/capability/**", + "contracts/artifact/**", + "contracts/epistemics/**", + "contracts/continuity/**", + "contracts/interaction/**", + "modules/work/**", + "modules/capability/**", + "modules/epistemics/**", + "modules/continuity/**", + "adapters/postgres/**", + "cmd/core-runtime/**", + "cmd/creator-web/**", + "cmd/continuity-runtime/**", + "database/migrations/**", + "deploy/docker-compose.dev.yml", + "deploy/docker-compose.prod.yml", + "deploy/windows/**", + "tests/**", + "docs/PROJECT_STATUS.md", + "architecture/adrs/ADR-0017-general-agent-delegation-codex-bridge.md", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000069-general-agent-delegation-codex-bridge.json" + ] + }, + "forbidden_change_scope": { + "paths": [ + "architecture/ARCHITECTURE_CONSTITUTION.md", + "architecture/architecture-registry.yaml", + "architecture/architecture-registry.json", + "modules/authority/**", + "modules/commit/**", + "cmd/commit-runtime/**", + "security/**", + "ci/**", + ".gitea/**" + ] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0, + "new_third_party_dependencies": 0, + "new_provider_adapters": 0, + "new_runtime_secret_file_for_existing_owner": "only if an already-approved bridge/runtime prerequisite requires a least-privilege credential" + }, + "contracts": { + "consume": [ + "Interaction HumanUtterance / OriginalHumanIntent", + "Situation focus and project resolution", + "Work Goal / PlanCandidate / CapabilityRequirement / DelegationCandidate", + "Capability CapabilityImplementation / CapabilityResolution / CapabilityUsageObservation", + "Continuity Attention / HumanResponse", + "Epistemics ExperienceRecord / ReflectionCandidate", + "Artifact metadata refs" + ], + "may_change": [ + "contracts/work@v1", + "contracts/capability@v1", + "contracts/artifact@v1", + "contracts/epistemics@v1" + ] + }, + "semantic_owner": "work", + "canonical_state": [ + "CapabilityImplementation(kind=AGENT)", + "Delegation", + "AgentRun", + "AgentCheckpoint", + "AgentResult" + ], + "state_transitions": [ + "OriginalHumanIntent -> optional Goal/Plan -> CapabilityRequirement -> real Delegation", + "Delegation -> AgentRun STARTED -> RUNNING/WAITING/CHECKPOINTED/EXECUTION_WINDOW_EXHAUSTED/BLOCKED/FAILED/COMPLETED/CANCELLED", + "execution window exhaustion -> durable checkpoint -> resumed run on same higher-level Delegation", + "human-dependent blocker -> AttentionItem -> natural HumanResponse -> continue delegation/work", + "agent result evidence -> verification -> Work completion decision -> ExperienceRecord" + ], + "side_effects_and_privileges": [ + "No new canonical owner and no Codex-specific canonical subsystem", + "No direct external mutation outside existing Authority/Commit boundaries", + "Agent-specific instruction is derived adapter context, not canonical human intent", + "Codex bridge may use only the least-privilege runtime/credential path required for bounded repository work", + "Docker-test remains prohibited" + ], + "persistence_and_migrations": [ + "owner-local Work and Capability schema extensions only", + "no cross-owner FK", + "no cross-owner SQL join dependency", + "result evidence stored by typed refs/metadata rather than payload duplication where possible" + ], + "failure_retry_restart": [ + "execution-window exhaustion is resumable and not automatic work failure", + "technical agent failure is distinct from human-dependent blocker", + "agent self-report is insufficient for Work completion without verification evidence", + "Codex unavailable must create capability unavailability/gap rather than startup failure" + ], + "assumptions": [ + "DEV-000068 production foundation is active", + "CapabilityImplementation(kind=AGENT) is sufficient for a general agent descriptor V1", + "Work is the minimal correct owner for durable delegation lifecycle", + "Continuity attention/human-response foundations from DEV-000066 can be reused for delegation waiting/blockers" + ], + "acceptance_scenarios": [ + "software-engineering requirement resolves through executor-neutral capability resolution", + "Codex is represented as an implementation, not canonical capability identity", + "delegation run checkpoints and resumes after execution-window exhaustion", + "human-dependent blocker surfaces through Attention while technical failure does not masquerade as human blocker", + "agent-specific execution instruction is derived from canonical context and OriginalHumanIntent remains unchanged", + "safe real self-development scenario creates delegation, run, checkpoint/result evidence and ExperienceRecord", + "LLM-free system starts when Codex bridge is unavailable" + ], + "required_tests": [ + "go test ./...", + "go vet ./...", + "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", + "go run ./cmd/devctl check-architecture", + "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", + "tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1", + "tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1", + "delegation/resume/attention/provider-neutral contract tests", + "production health", + "safe live Codex delegation acceptance" + ], + "terminal_gap_states": [ + "BLOCKED_BY_ARCHITECTURE_GAP", + "BLOCKED_BY_SPEC_GAP", + "BLOCKED_BY_MISSING_CAPABILITY", + "BLOCKED_BY_CONFLICT" + ] +} diff --git a/modules/continuity/domain/human_response.go b/modules/continuity/domain/human_response.go index 845e108..cf11d99 100644 --- a/modules/continuity/domain/human_response.go +++ b/modules/continuity/domain/human_response.go @@ -41,7 +41,7 @@ func InterpretHumanResponse(request contract.HumanResponseRequest, owner identit Revision: 1, UserReportedNotVerified: false, } - if operation == contract.ResponseUnknown || operation == contract.ResponseContinue || operation == contract.ResponseReject { + if operation == contract.ResponseUnknown { return contract.HumanResponseResult{Response: response, Handled: false}, nil } if len(candidates) == 0 { @@ -67,11 +67,22 @@ func InterpretHumanResponse(request contract.HumanResponseRequest, owner identit response.TargetKind = contract.ResponseTargetAttentionItem response.TargetAttentionID = candidate.Attention.ID response.TargetProspectiveID = candidate.Attention.SourceProspectiveID + response.TargetExternalRef = candidate.Attention.SourceExternalRef } else { response.TargetKind = contract.ResponseTargetProspectiveItem response.TargetProspectiveID = candidate.Prospective.ID } response.ResolutionState = contract.ResponseResolved + if response.TargetExternalRef != "" && candidate.Attention.SourceKind == contract.AttentionSourceAgentDelegation { + switch operation { + case contract.ResponseContinue: + response.Outcome = contract.ResponseOutcomeContinued + return contract.HumanResponseResult{Response: response, Handled: true, Reply: "Принял. Продолжаю работу по этой задаче."}, nil + case contract.ResponseReject: + response.Outcome = contract.ResponseOutcomeRejected + return contract.HumanResponseResult{Response: response, Handled: true, Reply: "Понял. Зафиксировал, что здесь нужно другое решение."}, nil + } + } switch operation { case contract.ResponseComplete: response.Outcome = contract.ResponseOutcomeUserReportedComplete diff --git a/modules/continuity/domain/human_response_test.go b/modules/continuity/domain/human_response_test.go index a44aa1e..ab1ef60 100644 --- a/modules/continuity/domain/human_response_test.go +++ b/modules/continuity/domain/human_response_test.go @@ -87,6 +87,42 @@ func TestInterpretHumanResponseNoTargetAsksClarification(t *testing.T) { } } +func TestInterpretHumanResponseContinuesAgentDelegationAttention(t *testing.T) { + result := interpretResponseForTest(t, "Продолжай", []ResponseCandidate{{ + Attention: continuity.AttentionItem{ + ID: "attention-1", + OwnerPrincipalID: "principal-1", + ScopeID: "scope-1", + SourceKind: continuity.AttentionSourceAgentDelegation, + SourceExternalRef: "work:delegation:delegation-1", + State: continuity.AttentionDelivered, + Summary: "нужен твой выбор по задаче coding-agent", + Revision: 1, + }, + }}) + if !result.Handled || result.Response.Operation != continuity.ResponseContinue || result.Response.Outcome != continuity.ResponseOutcomeContinued || result.Response.TargetExternalRef != "work:delegation:delegation-1" { + t.Fatalf("unexpected delegation continue response: %#v", result) + } +} + +func TestInterpretHumanResponseRejectsAgentDelegationAttention(t *testing.T) { + result := interpretResponseForTest(t, "Нет, нужен другой вариант", []ResponseCandidate{{ + Attention: continuity.AttentionItem{ + ID: "attention-1", + OwnerPrincipalID: "principal-1", + ScopeID: "scope-1", + SourceKind: continuity.AttentionSourceAgentDelegation, + SourceExternalRef: "work:delegation:delegation-1", + State: continuity.AttentionDelivered, + Summary: "нужен твой выбор по задаче coding-agent", + Revision: 1, + }, + }}) + if !result.Handled || result.Response.Operation != continuity.ResponseReject || result.Response.Outcome != continuity.ResponseOutcomeRejected { + t.Fatalf("unexpected delegation reject response: %#v", result) + } +} + func interpretResponseForTest(t *testing.T, message string, candidates []ResponseCandidate) continuity.HumanResponseResult { t.Helper() result, err := InterpretHumanResponse(continuity.HumanResponseRequest{ diff --git a/modules/work/domain/delegation.go b/modules/work/domain/delegation.go new file mode 100644 index 0000000..9634967 --- /dev/null +++ b/modules/work/domain/delegation.go @@ -0,0 +1,326 @@ +package domain + +import ( + "errors" + "strings" + "time" + + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/work" + "git.cin.su/m/Assistant/platform/ids" +) + +var ErrInvalidDelegation = errors.New("invalid delegation") +var ErrInvalidAgentRun = errors.New("invalid agent run") +var ErrInvalidAgentCheckpoint = errors.New("invalid agent checkpoint") +var ErrInvalidAgentResult = errors.New("invalid agent result") +var ErrInvalidDelegationVerification = errors.New("invalid delegation verification") + +type DelegationRequest struct { + Goal work.Goal + Plan work.WorkPlan + Candidate work.PlanCandidate + Requirement work.CapabilityRequirement + Implementation capability.CapabilityImplementation + ConversationID work.ConversationRef + OriginalIntentText string + ContextSummary string + ContextRefs []string + VerificationSummary string + DerivedInstruction string + StartedAt time.Time +} + +type DelegationBundle struct { + Delegation work.Delegation + Run work.AgentRun +} + +func StartDelegation(input DelegationRequest) (DelegationBundle, error) { + if input.Goal.ID == "" || input.Plan.ID == "" || input.Candidate.ID == "" || input.Requirement.ID == "" || + input.Implementation.ID == "" || input.Implementation.Kind != capability.ImplementationAgent || + input.Requirement.CapabilityKind == "" || input.StartedAt.IsZero() || strings.TrimSpace(input.OriginalIntentText) == "" { + return DelegationBundle{}, ErrInvalidDelegation + } + delegationID, err := ids.New() + if err != nil { + return DelegationBundle{}, err + } + runID, err := ids.New() + if err != nil { + return DelegationBundle{}, err + } + millis := input.StartedAt.UnixMilli() + delegation := work.Delegation{ + ID: work.DelegationID(delegationID), + GoalID: input.Goal.ID, + PlanID: input.Plan.ID, + PlanCandidateID: input.Candidate.ID, + CapabilityRequirementID: input.Requirement.ID, + CapabilityKind: input.Requirement.CapabilityKind, + SelectedImplementationID: input.Implementation.ID, + SelectedImplementationRev: input.Implementation.Revision, + SelectedImplementationKind: input.Implementation.Kind, + Protocol: protocolForImplementation(input.Implementation), + SourceIntentID: input.Goal.SourceIntentID, + SourceUtteranceID: input.Goal.SourceUtteranceID, + ConversationID: input.ConversationID, + ProjectID: input.Goal.ProjectID, + ProjectDisplayName: input.Goal.ProjectDisplayName, + DesiredOutcome: input.Goal.DesiredOutcome, + ConstraintSummary: nonEmpty(input.Goal.Constraints, "no additional constraints"), + VerificationSummary: nonEmpty(input.VerificationSummary, "verify outcome through tests, repository state, and explicit evidence"), + ContextSummary: buildContextSummary(input), + ContextRefs: append([]string{}, input.ContextRefs...), + State: work.DelegationRunning, + LatestRunID: work.AgentRunID(runID), + CreatedAtUnixMillis: millis, + UpdatedAtUnixMillis: millis, + } + run := work.AgentRun{ + ID: work.AgentRunID(runID), + DelegationID: delegation.ID, + ImplementationID: input.Implementation.ID, + ImplementationRevision: input.Implementation.Revision, + Protocol: delegation.Protocol, + State: work.AgentRunStarted, + DerivedInstructionSummary: summarizeInstruction(input.DerivedInstruction), + StartedAtUnixMillis: millis, + UpdatedAtUnixMillis: millis, + } + if err := ValidateDelegation(delegation); err != nil { + return DelegationBundle{}, err + } + if err := ValidateAgentRun(run); err != nil { + return DelegationBundle{}, err + } + return DelegationBundle{Delegation: delegation, Run: run}, nil +} + +func ValidateDelegation(value work.Delegation) error { + if value.ID == "" || value.GoalID == "" || value.PlanID == "" || value.PlanCandidateID == "" || + value.CapabilityRequirementID == "" || value.CapabilityKind == "" || value.SelectedImplementationID == "" || + value.SelectedImplementationRev < 1 || value.SelectedImplementationKind != capability.ImplementationAgent || + value.Protocol == "" || value.SourceIntentID == "" || value.SourceUtteranceID == "" || + strings.TrimSpace(value.DesiredOutcome) == "" || value.State == "" || value.CreatedAtUnixMillis < 1 || + value.UpdatedAtUnixMillis < value.CreatedAtUnixMillis { + return ErrInvalidDelegation + } + return nil +} + +func ValidateAgentRun(value work.AgentRun) error { + if value.ID == "" || value.DelegationID == "" || value.ImplementationID == "" || value.ImplementationRevision < 1 || + value.Protocol == "" || value.State == "" || value.StartedAtUnixMillis < 1 || value.UpdatedAtUnixMillis < value.StartedAtUnixMillis { + return ErrInvalidAgentRun + } + return nil +} + +func CheckpointRun(run work.AgentRun, blocker work.AgentBlockerClass, completed, remaining, tests, branch, commit, next string, refs []work.EvidenceRef, now time.Time) (work.AgentRun, work.AgentCheckpoint, error) { + if err := ValidateAgentRun(run); err != nil || now.IsZero() { + return work.AgentRun{}, work.AgentCheckpoint{}, ErrInvalidAgentRun + } + checkpointID, err := ids.New() + if err != nil { + return work.AgentRun{}, work.AgentCheckpoint{}, err + } + state := work.AgentRunCheckpointed + switch blocker { + case work.AgentBlockerExecutionWindow: + state = work.AgentRunExecutionWindowExhausted + case work.AgentBlockerHumanDecisionRequired, work.AgentBlockerMissingExternalCredential, work.AgentBlockerArchitectureDecision, work.AgentBlockerSecurityConflict: + state = work.AgentRunBlocked + case work.AgentBlockerTechnicalFailure: + state = work.AgentRunFailed + case work.AgentBlockerNone: + state = work.AgentRunCheckpointed + default: + return work.AgentRun{}, work.AgentCheckpoint{}, ErrInvalidAgentCheckpoint + } + run.State = state + run.UpdatedAtUnixMillis = now.UnixMilli() + checkpoint := work.AgentCheckpoint{ + ID: work.AgentCheckpointID(checkpointID), + DelegationID: run.DelegationID, + RunID: run.ID, + State: state, + BlockerClass: blocker, + CompletedSummary: strings.TrimSpace(completed), + RemainingSummary: strings.TrimSpace(remaining), + TestSummary: strings.TrimSpace(tests), + BranchName: strings.TrimSpace(branch), + CommitHash: strings.TrimSpace(commit), + NextStep: strings.TrimSpace(next), + ArtifactRefs: append([]work.EvidenceRef{}, refs...), + RecordedAtUnixMillis: now.UnixMilli(), + } + if err := ValidateAgentCheckpoint(checkpoint); err != nil { + return work.AgentRun{}, work.AgentCheckpoint{}, err + } + return run, checkpoint, nil +} + +func ValidateAgentCheckpoint(value work.AgentCheckpoint) error { + if value.ID == "" || value.DelegationID == "" || value.RunID == "" || value.State == "" || value.BlockerClass == "" || value.RecordedAtUnixMillis < 1 { + return ErrInvalidAgentCheckpoint + } + return nil +} + +func CompleteRun(run work.AgentRun, summary, outcomeRef, verification string, refs []work.EvidenceRef, now time.Time) (work.AgentRun, work.AgentResult, error) { + if err := ValidateAgentRun(run); err != nil || now.IsZero() || strings.TrimSpace(summary) == "" { + return work.AgentRun{}, work.AgentResult{}, ErrInvalidAgentResult + } + resultID, err := ids.New() + if err != nil { + return work.AgentRun{}, work.AgentResult{}, err + } + run.State = work.AgentRunCompleted + run.UpdatedAtUnixMillis = now.UnixMilli() + run.CompletedAtUnixMillis = now.UnixMilli() + result := work.AgentResult{ + ID: work.AgentResultID(resultID), + DelegationID: run.DelegationID, + RunID: run.ID, + State: work.AgentRunCompleted, + Summary: strings.TrimSpace(summary), + OutcomeRef: strings.TrimSpace(outcomeRef), + VerificationRequired: true, + VerificationSummary: nonEmpty(verification, "verify result before higher-level work completion"), + ArtifactRefs: append([]work.EvidenceRef{}, refs...), + RecordedAtUnixMillis: now.UnixMilli(), + } + if err := ValidateAgentResult(result); err != nil { + return work.AgentRun{}, work.AgentResult{}, err + } + return run, result, nil +} + +func ValidateAgentResult(value work.AgentResult) error { + if value.ID == "" || value.DelegationID == "" || value.RunID == "" || value.State == "" || + strings.TrimSpace(value.Summary) == "" || value.RecordedAtUnixMillis < 1 { + return ErrInvalidAgentResult + } + return nil +} + +func ResumeDelegationRun(delegation work.Delegation, previous work.AgentRun, checkpoint work.AgentCheckpoint, now time.Time) (work.Delegation, work.AgentRun, error) { + if err := ValidateDelegation(delegation); err != nil { + return work.Delegation{}, work.AgentRun{}, ErrInvalidAgentRun + } + if err := ValidateAgentRun(previous); err != nil { + return work.Delegation{}, work.AgentRun{}, ErrInvalidAgentRun + } + if err := ValidateAgentCheckpoint(checkpoint); err != nil || now.IsZero() { + return work.Delegation{}, work.AgentRun{}, ErrInvalidAgentRun + } + if delegation.ID != previous.DelegationID || checkpoint.RunID != previous.ID { + return work.Delegation{}, work.AgentRun{}, ErrInvalidAgentRun + } + if previous.State != work.AgentRunExecutionWindowExhausted && previous.State != work.AgentRunBlocked && previous.State != work.AgentRunCheckpointed { + return work.Delegation{}, work.AgentRun{}, ErrInvalidAgentRun + } + runID, err := ids.New() + if err != nil { + return work.Delegation{}, work.AgentRun{}, err + } + delegation.State = work.DelegationRunning + delegation.UpdatedAtUnixMillis = now.UnixMilli() + delegation.LatestRunID = work.AgentRunID(runID) + run := work.AgentRun{ + ID: work.AgentRunID(runID), + DelegationID: delegation.ID, + ImplementationID: previous.ImplementationID, + ImplementationRevision: previous.ImplementationRevision, + Protocol: previous.Protocol, + State: work.AgentRunStarted, + DerivedInstructionSummary: summarizeInstruction(checkpoint.NextStep), + StartedAtUnixMillis: now.UnixMilli(), + UpdatedAtUnixMillis: now.UnixMilli(), + } + if err := ValidateAgentRun(run); err != nil { + return work.Delegation{}, work.AgentRun{}, err + } + return delegation, run, nil +} + +func VerifyDelegationResult(delegation work.Delegation, run work.AgentRun, result work.AgentResult, verified bool, summary string, refs []work.EvidenceRef, now time.Time) (work.Delegation, work.DelegationVerification, error) { + if err := ValidateDelegation(delegation); err != nil { + return work.Delegation{}, work.DelegationVerification{}, ErrInvalidDelegationVerification + } + if err := ValidateAgentRun(run); err != nil { + return work.Delegation{}, work.DelegationVerification{}, ErrInvalidDelegationVerification + } + if err := ValidateAgentResult(result); err != nil || now.IsZero() || strings.TrimSpace(summary) == "" { + return work.Delegation{}, work.DelegationVerification{}, ErrInvalidDelegationVerification + } + verificationID, err := ids.New() + if err != nil { + return work.Delegation{}, work.DelegationVerification{}, err + } + if verified { + delegation.State = work.DelegationCompleted + } else { + delegation.State = work.DelegationFailed + } + delegation.UpdatedAtUnixMillis = now.UnixMilli() + verification := work.DelegationVerification{ + ID: work.DelegationVerificationID(verificationID), + DelegationID: delegation.ID, + RunID: run.ID, + ResultID: result.ID, + Verified: verified, + Summary: strings.TrimSpace(summary), + EvidenceRefs: append([]work.EvidenceRef{}, refs...), + RecordedAtUnixMillis: now.UnixMilli(), + } + if err := ValidateDelegationVerification(verification); err != nil { + return work.Delegation{}, work.DelegationVerification{}, err + } + return delegation, verification, nil +} + +func ValidateDelegationVerification(value work.DelegationVerification) error { + if value.ID == "" || value.DelegationID == "" || value.RunID == "" || value.ResultID == "" || strings.TrimSpace(value.Summary) == "" || value.RecordedAtUnixMillis < 1 { + return ErrInvalidDelegationVerification + } + return nil +} + +func protocolForImplementation(value capability.CapabilityImplementation) work.AgentExecutionProtocol { + if strings.HasPrefix(string(value.TargetRef), "codex://") { + return work.AgentProtocolCodexBridgeV1 + } + return work.AgentProtocolCodexBridgeV1 +} + +func buildContextSummary(input DelegationRequest) string { + parts := []string{} + if input.Goal.ProjectDisplayName != "" { + parts = append(parts, "project="+input.Goal.ProjectDisplayName) + } + parts = append(parts, "human_intent="+strings.TrimSpace(input.OriginalIntentText)) + if text := strings.TrimSpace(input.ContextSummary); text != "" { + parts = append(parts, "added_context="+text) + } + return strings.Join(parts, "; ") +} + +func summarizeInstruction(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "derived execution context prepared from canonical intent, project context, and verification requirements" + } + if len(value) > 500 { + return value[:500] + } + return value +} + +func nonEmpty(value, fallback string) string { + if strings.TrimSpace(value) == "" { + return fallback + } + return strings.TrimSpace(value) +} diff --git a/modules/work/domain/delegation_test.go b/modules/work/domain/delegation_test.go new file mode 100644 index 0000000..e9757b1 --- /dev/null +++ b/modules/work/domain/delegation_test.go @@ -0,0 +1,115 @@ +package domain + +import ( + "testing" + "time" + + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/work" +) + +func TestStartDelegationPreservesCanonicalIntentAndUsesAgentImplementation(t *testing.T) { + now := time.Unix(170, 0) + bundle, err := StartDelegation(DelegationRequest{ + Goal: work.Goal{ + ID: "goal-1", + SourceIntentID: "intent-1", + SourceUtteranceID: "utterance-1", + ProjectID: "assistant", + ProjectDisplayName: "Assistant", + DesiredOutcome: "fix the human-facing chat wording", + Constraints: "follow architecture constitution", + }, + Plan: work.WorkPlan{ID: "plan-1"}, + Candidate: work.PlanCandidate{ID: "candidate-1"}, + Requirement: work.CapabilityRequirement{ID: "requirement-1", CapabilityKind: capability.CapabilityKindModifyCode}, + Implementation: agentImplementation("codex-impl-1", "codex://assistant"), + ConversationID: "conversation-1", + OriginalIntentText: "Исправь текст в чате Assistant.", + ContextSummary: "relevant files: creator-web chat surface", + ContextRefs: []string{"interaction:intent:intent-1", "situation:project:assistant"}, + VerificationSummary: "run targeted tests and verify reply text", + DerivedInstruction: "Open the current branch, inspect chat surface, implement minimal wording fix, run tests.", + StartedAt: now, + }) + if err != nil { + t.Fatalf("start delegation: %v", err) + } + if bundle.Delegation.SelectedImplementationKind != capability.ImplementationAgent { + t.Fatalf("delegation must keep AGENT implementation kind: %#v", bundle.Delegation) + } + if bundle.Delegation.SourceIntentID != "intent-1" || bundle.Delegation.SourceUtteranceID != "utterance-1" { + t.Fatalf("canonical human refs lost: %#v", bundle.Delegation) + } + if bundle.Run.Protocol != work.AgentProtocolCodexBridgeV1 { + t.Fatalf("expected codex bridge protocol, got %#v", bundle.Run.Protocol) + } + if bundle.Run.DerivedInstructionSummary == "" { + t.Fatalf("derived instruction summary must be present") + } +} + +func TestCheckpointExecutionWindowExhaustionIsNotFailure(t *testing.T) { + run := work.AgentRun{ + ID: "run-1", + DelegationID: "delegation-1", + ImplementationID: "impl-1", + ImplementationRevision: 1, + Protocol: work.AgentProtocolCodexBridgeV1, + State: work.AgentRunRunning, + StartedAtUnixMillis: 1, + UpdatedAtUnixMillis: 1, + } + updated, checkpoint, err := CheckpointRun(run, work.AgentBlockerExecutionWindow, "inspected files", "apply patch and run tests", "go test pending", "codex/dev-000069", "abc123", "continue with next execution window", []work.EvidenceRef{"artifact:diff:1"}, time.Unix(200, 0)) + if err != nil { + t.Fatalf("checkpoint: %v", err) + } + if updated.State != work.AgentRunExecutionWindowExhausted { + t.Fatalf("execution window exhaustion must be explicit state, got %#v", updated.State) + } + if checkpoint.BlockerClass != work.AgentBlockerExecutionWindow { + t.Fatalf("checkpoint blocker mismatch: %#v", checkpoint) + } +} + +func TestDifferentFutureAgentStillSatisfiesSameCapabilityContract(t *testing.T) { + now := time.Unix(170, 0) + for _, implementation := range []capability.CapabilityImplementation{ + agentImplementation("codex-impl-1", "codex://assistant"), + agentImplementation("future-impl-1", "research-agent://cluster-a"), + } { + bundle, err := StartDelegation(DelegationRequest{ + Goal: work.Goal{ID: "goal-1", SourceIntentID: "intent-1", SourceUtteranceID: "utterance-1", DesiredOutcome: "do the work"}, + Plan: work.WorkPlan{ID: "plan-1"}, + Candidate: work.PlanCandidate{ID: "candidate-1"}, + Requirement: work.CapabilityRequirement{ID: "requirement-1", CapabilityKind: capability.CapabilityKindModifyCode}, + Implementation: implementation, + OriginalIntentText: "Сделай безопасное изменение.", + StartedAt: now, + }) + if err != nil { + t.Fatalf("start delegation for %s: %v", implementation.ID, err) + } + if bundle.Delegation.CapabilityKind != capability.CapabilityKindModifyCode { + t.Fatalf("capability contract changed for %s: %#v", implementation.ID, bundle.Delegation) + } + } +} + +func agentImplementation(id, target string) capability.CapabilityImplementation { + return capability.CapabilityImplementation{ + ID: capability.CapabilityImplementationID(id), + CapabilityID: "modify-code", + CapabilityKind: capability.CapabilityKindModifyCode, + OwnerPrincipalID: "domovoy-creator-principal", + Revision: 1, + Kind: capability.ImplementationAgent, + TargetRef: capability.ImplementationTargetRef(target), + Qualification: capability.QualificationValidated, + Activation: capability.ActivationCanary, + Health: capability.HealthAvailable, + AuthorityPolicyRef: capability.AuthorityPolicyCreatorDirectiveV1, + DataConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly, capability.DataConstraintNoCredentialForward}, + ObservedAtUnixMillis: 1, + } +} -- 2.54.0