DEV-000069 continuation: agent delegation response candidates #64

Merged
m merged 2 commits from codex/general-agent-delegation-codex-bridge-v1 into main 2026-08-17 17:25:40 +00:00
2 changed files with 104 additions and 2 deletions
+71 -2
View File
@@ -234,6 +234,35 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
if owner == "" || limit < 1 || limit > 50 {
return nil, fmt.Errorf("invalid response candidate query")
}
candidates := make([]continuitydomain.ResponseCandidate, 0, limit)
attentionRows, err := r.pool.Query(ctx, `SELECT
attention_item_id,owner_principal_id,scope_id,source_kind,source_prospective_item_id,source_external_ref,level,state,summary,
due_at_unix_millis,created_at_unix_millis,delivered_at_unix_millis,action_intent_id,authorized_action_id,action_attempt_id,delivery_outcome,
response_id,response_utterance_id,response_operation,response_outcome,responded_at_unix_millis,revision
FROM continuity.attention_items
WHERE owner_principal_id=$1
AND source_kind=$2
AND state IN ($3,$4,$5)
ORDER BY created_at_unix_millis DESC, attention_item_id DESC
LIMIT $6`,
owner, continuity.AttentionSourceAgentDelegation, continuity.AttentionPending, continuity.AttentionDelivered, continuity.AttentionFailed, limit)
if err != nil {
return nil, fmt.Errorf("list delegation response candidates: %w", err)
}
defer attentionRows.Close()
for attentionRows.Next() {
item, err := scanAttentionItem(attentionRows)
if err != nil {
return nil, fmt.Errorf("scan delegation response candidate: %w", err)
}
candidates = append(candidates, continuitydomain.ResponseCandidate{Attention: item})
}
if err := attentionRows.Err(); err != nil {
return nil, fmt.Errorf("list delegation response candidates: %w", err)
}
if len(candidates) >= limit {
return candidates[:limit], nil
}
rows, err := r.pool.Query(ctx, `SELECT
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,''),
@@ -247,12 +276,11 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
ORDER BY p.recorded_at DESC, COALESCE(a.delivered_at_unix_millis,0) DESC, p.prospective_item_id DESC
LIMIT $8`,
owner, continuity.AttentionSourceProspectiveReminder, continuity.AttentionDelivered, continuity.AttentionPending,
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit)
continuity.AttentionFailed, continuity.ProspectiveDelivered, continuity.ProspectiveArmed, limit-len(candidates))
if err != nil {
return nil, fmt.Errorf("list human response candidates: %w", err)
}
defer rows.Close()
var candidates []continuitydomain.ResponseCandidate
for rows.Next() {
var candidate continuitydomain.ResponseCandidate
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, sourceExternalRef, level, attentionState, attentionSummary string
@@ -303,6 +331,47 @@ func (r *ContinuityRepository) ResponseCandidates(ctx context.Context, owner ide
return candidates, nil
}
func scanAttentionItem(scanner interface {
Scan(...any) error
}) (continuity.AttentionItem, error) {
var item continuity.AttentionItem
var attentionID, attentionOwner, attentionScope, sourceKind, sourceProspective, sourceExternalRef, level, attentionState, attentionSummary string
var actionIntent, authorizedAction, actionAttempt, deliveryOutcome, responseID, responseUtterance, responseOperation, responseOutcome string
var dueAt, createdAt, deliveredAt, respondedAt int64
var attentionRevision uint64
if err := scanner.Scan(
&attentionID, &attentionOwner, &attentionScope, &sourceKind, &sourceProspective, &sourceExternalRef, &level, &attentionState, &attentionSummary,
&dueAt, &createdAt, &deliveredAt, &actionIntent, &authorizedAction, &actionAttempt, &deliveryOutcome,
&responseID, &responseUtterance, &responseOperation, &responseOutcome, &respondedAt, &attentionRevision); err != nil {
return continuity.AttentionItem{}, err
}
item = continuity.AttentionItem{
ID: continuity.AttentionItemID(attentionID),
OwnerPrincipalID: identity.PrincipalID(attentionOwner),
ScopeID: authority.ScopeID(attentionScope),
SourceKind: continuity.AttentionSourceKind(sourceKind),
SourceProspectiveID: continuity.ProspectiveItemID(sourceProspective),
SourceExternalRef: sourceExternalRef,
Level: continuity.AttentionLevel(level),
State: continuity.AttentionState(attentionState),
Summary: attentionSummary,
DueAtUnixMillis: dueAt,
CreatedAtUnixMillis: createdAt,
DeliveredAtUnixMillis: deliveredAt,
ActionIntentID: continuity.CommittedActionIntentID(actionIntent),
AuthorizedActionID: continuity.CommittedAuthorizedActionID(authorizedAction),
ActionAttemptID: continuity.CommittedActionAttemptID(actionAttempt),
DeliveryOutcome: deliveryOutcome,
ResponseID: continuity.HumanResponseID(responseID),
ResponseUtteranceID: responseUtterance,
ResponseOperation: continuity.HumanResponseOperation(responseOperation),
ResponseOutcome: continuity.ResponseOutcome(responseOutcome),
RespondedAtUnixMillis: respondedAt,
Revision: continuity.AttentionRevision(attentionRevision),
}
return item, nil
}
func (r *ContinuityRepository) ApplyHumanResponse(ctx context.Context, response continuity.HumanResponse) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
@@ -65,6 +65,39 @@ func TestCreatorResponseEndpointRecordsAmbiguousWithoutSilentTargetMutation(t *t
}
}
func TestCreatorResponseEndpointContinuesDelegationAttentionViaNaturalMessage(t *testing.T) {
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
}))
defer core.Close()
identityClient, err := newCreatorIdentityClient(core.URL, core.Client())
if err != nil {
t.Fatal(err)
}
repository := &fakeResponseRepository{candidates: []continuitydomain.ResponseCandidate{
{Attention: continuity.AttentionItem{
ID: "work:delegation:delegation-1",
OwnerPrincipalID: "principal-1",
ScopeID: "scope-1",
SourceKind: continuity.AttentionSourceAgentDelegation,
SourceExternalRef: "work:delegation:delegation-1",
State: continuity.AttentionPending,
Summary: "Нужен твой выбор по задаче coding-agent: docs/PROJECT_STATUS.md",
Revision: 1,
}},
{Attention: continuity.AttentionItem{ID: "attention-1", SourceProspectiveID: "prospective-1", State: continuity.AttentionDelivered, Summary: "старое напоминание"}},
}}
mux := http.NewServeMux()
registerCreatorResponseRoute(mux, identityClient, repository, func() time.Time { return time.Unix(100, 0) })
request := httptest.NewRequest(http.MethodPost, "/v1/creator/responses", strings.NewReader(`{"SourceUtteranceID":"utterance-1","SourceIntentID":"intent-1","Message":"Продолжай задачу coding-agent по docs/PROJECT_STATUS.md","RequestedAtUnixMillis":100000}`))
request.Header.Set("Authorization", "Bearer ok")
recorder := httptest.NewRecorder()
mux.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated || repository.applied.Operation != continuity.ResponseContinue || repository.applied.TargetExternalRef != "work:delegation:delegation-1" || repository.applied.Outcome != continuity.ResponseOutcomeContinued {
t.Fatalf("status=%d applied=%#v body=%s", recorder.Code, repository.applied, recorder.Body.String())
}
}
type fakeResponseRepository struct {
candidates []continuitydomain.ResponseCandidate
applied continuity.HumanResponse