Ctrl K
ComunidadGuías…

internal/approvalgate/permission_asker.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package approvalgate

import (
	"context"
	"log/slog"
	"strings"
	"time"

	"github.com/yeomyeonggeori/blueclaw/internal/mcpserver"
	"github.com/yeomyeonggeori/blueprotocol/agentcontract"
	"github.com/yeomyeonggeori/blueprotocol/holdrecord"
	"github.com/yeomyeonggeori/blueprotocol/toolcontract"
)

type AskStatus string

const (
	AskAnswered    AskStatus = "answered"
	AskUnreachable AskStatus = "unreachable"
	AskExpired     AskStatus = "expired"
	AskInterrupted AskStatus = "interrupted"
)

type PermissionAsker interface {
	AskPermission(ctx context.Context, approvalRequest mcpserver.ApprovalRequest, question PermissionQuestion) (ApprovalAnswer, AskStatus)
}

type PermissionQuestion struct {
	HoldID       string
	Confirmation string
	Choices      []holdrecord.Choice
	HandoffID    string
}

func (gate *Gate) UsePermissionAsker(permissionAsker PermissionAsker) {
	gate.permissionAsker = permissionAsker
}

func (gate *Gate) askedOutcome(ctx context.Context, taskRunID string, approvalRequest mcpserver.ApprovalRequest, confirmation string, resolution ApprovalTargetResolution) mcpserver.ApprovalOutcome {
	if gate.permissionAsker == nil || taskRunID == "" {
		return gate.unreachableOutcome(taskRunID, approvalRequest, "no_asker")
	}
	profileName := gate.currentAgentProfileName(taskRunID)
	hold, isHeld := gate.holdCall(taskRunID, approvalRequest, confirmation, resolution)
	if !isHeld {
		return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionUnanswerable}
	}
	answer, status := gate.permissionAsker.AskPermission(ctx, approvalRequest, PermissionQuestion{HoldID: hold.ID, Confirmation: confirmation, Choices: resolution.Choices, HandoffID: hold.Call.HandoffID})
	switch status {
	case AskAnswered:
		return gate.answeredOutcome(ctx, taskRunID, approvalRequest, confirmation, answer)
	case AskUnreachable:
		gate.endUnansweredHold(taskRunID, profileName, status)
		return gate.unreachableOutcome(taskRunID, approvalRequest, string(status))
	case AskExpired:
		gate.endUnansweredHold(taskRunID, profileName, status)
		gate.taskRunService.AppendTaskEvent(taskRunID, TaskEventApprovalExpired, marshalEventBody(map[string]string{"toolName": strings.TrimSpace(approvalRequest.ToolName), "requesterPersonID": approvalRequest.RequesterPersonID}))
		return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionRejected, Notice: expiredCallNotice}
	}
	return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionHeld, Notice: confirmation}
}

func (gate *Gate) answeredOutcome(ctx context.Context, taskRunID string, approvalRequest mcpserver.ApprovalRequest, confirmation string, answer ApprovalAnswer) mcpserver.ApprovalOutcome {
	settlement, errorValue := gate.SettleAnswer(ctx, approvalRequest, answer)
	switch {
	case errorValue != nil:
		slog.Warn("approvalgate.answered_run_will_not_advance", "taskRunID", taskRunID, "toolName", strings.TrimSpace(approvalRequest.ToolName), "reason", errorValue.Error())
		return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionHeld, Notice: confirmation}
	case settlement.DeferredCall != nil:
		return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionDeferred, Deferral: settlement.DeferredCall.Result}
	case settlement.Signal == agentcontract.ApprovalSignalReject:
		return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionRejected}
	}
	return gate.approvedOutcome(taskRunID, approvalRequest)
}

type AnswerSettlement struct {
	Signal       agentcontract.ApprovalSignal
	DeferredCall *agentcontract.CarriedOutCall
}

func (gate *Gate) SettleAnswer(ctx context.Context, approvalRequest mcpserver.ApprovalRequest, answer ApprovalAnswer) (AnswerSettlement, error) {
	taskRunID := strings.TrimSpace(approvalRequest.TaskRunID)
	taskEvents := gate.taskRunService.ListTaskEvent(taskRunID)
	if _, errorValue := gate.taskRunService.AdvanceTaskRun(taskRunID, gate.currentAgentProfileName(taskRunID)); errorValue != nil {
		return AnswerSettlement{}, errorValue
	}
	heldCall, _ := PendingHeldCall(taskEvents)
	choice, isChosen := answer.ChosenFrom(OfferedChoices(taskEvents))
	if isChosen && choice.DefersTheCall() {
		return AnswerSettlement{Signal: answer.Signal, DeferredCall: gate.deferHeldCall(ctx, approvalRequest, heldCall, choice)}, nil
	}
	if isChosen {
		RecordChoiceAnswer(gate.taskRunService, taskRunID, choice)
	}
	SettleSignal(gate.taskRunService, taskRunID, &answer.Signal, "acp_permission")
	return AnswerSettlement{Signal: answer.Signal}, nil
}

func (gate *Gate) deferHeldCall(ctx context.Context, approvalRequest mcpserver.ApprovalRequest, heldCall agentcontract.HeldCall, choice holdrecord.Choice) *agentcontract.CarriedOutCall {
	carriedOutCall := DeferHeldCall(ctx, gate, heldCall, DeferralRequest{
		TaskRunID:         approvalRequest.TaskRunID,
		RequesterPersonID: approvalRequest.RequesterPersonID,
		Platform:          approvalRequest.Platform,
		ConversationID:    approvalRequest.ConversationID,
		ReplyTargetID:     approvalRequest.ReplyTargetID,
		Prompt:            approvalRequest.Prompt,
		Choice:            choice,
		ReferenceTime:     time.Now().UTC(),
	})
	return &carriedOutCall
}

func (gate *Gate) endUnansweredHold(taskRunID string, profileName string, status AskStatus) {
	rejection := agentcontract.ApprovalSignalReject
	SettleSignal(gate.taskRunService, taskRunID, &rejection, string(status))
	if _, errorValue := gate.taskRunService.AdvanceTaskRun(taskRunID, profileName); errorValue != nil {
		slog.Warn("approvalgate.unanswered_run_will_not_advance", "taskRunID", taskRunID, "status", string(status), "reason", errorValue.Error())
	}
}

func (gate *Gate) unreachableOutcome(taskRunID string, approvalRequest mcpserver.ApprovalRequest, reason string) mcpserver.ApprovalOutcome {
	slog.Warn("approvalgate.requester_cannot_be_asked", "taskRunID", taskRunID, "toolName", strings.TrimSpace(approvalRequest.ToolName), "requesterPersonID", approvalRequest.RequesterPersonID, "reason", reason)
	if taskRunID != "" {
		gate.taskRunService.AppendTaskEvent(taskRunID, TaskEventApprovalUnreachable, marshalEventBody(map[string]string{
			"toolName":          strings.TrimSpace(approvalRequest.ToolName),
			"requesterPersonID": approvalRequest.RequesterPersonID,
			"reason":            reason,
		}))
	}
	return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionUnanswerable}
}

func DeferralFailedResult(errorValue error) toolcontract.ToolResult {
	return toolcontract.ToolFailureResult(toolcontract.FailureUnknown, toolcontract.FailureCodes.OperationFailed, "approval", "The requester chose to run this call later, and it could not be scheduled, so nothing will run: "+errorValue.Error())
}

func (gate *Gate) currentAgentProfileName(taskRunID string) string {
	taskRun, isFound := gate.taskRunService.FindTaskRun(taskRunID)
	if !isFound {
		return defaultAgentProfileName
	}
	if profileName := strings.TrimSpace(taskRun.CurrentAgentProfileName); profileName != "" {
		return profileName
	}
	return defaultAgentProfileName
}

const (
	defaultAgentProfileName      = "default"
	TaskEventApprovalUnreachable = "approval.unreachable"
	TaskEventApprovalExpired     = "approval.expired"
	expiredCallNotice            = "Nobody answered the requester's approval question within 24 hours, so this call was not approved and will not run. Do not retry it; tell the requester it did not run because the question went unanswered, and what they can ask for instead."
)