Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

230 lines
9.4 KiB
Go

package tuttimodeplan
import (
"context"
"errors"
"fmt"
"slices"
"strings"
workspaceissues "github.com/tutti-os/tutti/packages/workspace/issues"
executionbiz "github.com/tutti-os/tutti/services/tuttid/biz/tuttimodeexecution"
workflowbiz "github.com/tutti-os/tutti/services/tuttid/biz/workspaceworkflow"
workspaceservice "github.com/tutti-os/tutti/services/tuttid/service/workspace"
)
var ErrIssueMaterializationConflict = errors.New("tutti mode plan Issue materialization conflicts with an existing Issue")
// WorkspaceIssueMaterializer is the explicit integration adapter from an
// accepted Tutti Mode Plan revision to the Workspace Issue execution domain.
// The composition root only injects the Issue Manager; mapping and retry
// policy remain local to the workflow capability that owns the handoff.
type WorkspaceIssueTarget interface {
CreateIssueFromPlan(context.Context, string, workspaceservice.CreateIssueManagerIssueFromPlanInput) (workspaceissues.IssueDetail, error)
GetIssueDetail(context.Context, string, string) (workspaceissues.IssueDetail, error)
GetTuttiModeExecutionByIssue(context.Context, string, string) (executionbiz.Aggregate, error)
}
type WorkspaceIssueMaterializer struct {
Issues WorkspaceIssueTarget
}
func (materializer WorkspaceIssueMaterializer) MaterializeIssue(
ctx context.Context,
input MaterializeIssueInput,
) (string, error) {
if materializer.Issues == nil {
return "", ErrServiceUnavailable
}
issueID, ok := workflowbiz.TuttiModePlanIssueID(input.WorkflowID)
if !ok || strings.TrimSpace(input.RevisionID) == "" || strings.TrimSpace(input.SourceSessionID) == "" {
return "", ErrInvalidInput
}
if !actionableItemsHaveCanonicalOrder(input.ActionableItems) {
return "", ErrInvalidInput
}
input.Review.Mode = strings.ToLower(strings.TrimSpace(input.Review.Mode))
input.Review.AgentTargetID = strings.TrimSpace(input.Review.AgentTargetID)
if input.Review.Mode == "" {
input.Review.Mode = "self"
}
if (input.Review.Mode == "self" && input.Review.AgentTargetID != "") ||
(input.Review.Mode == "independent" && input.Review.AgentTargetID == "") ||
(input.Review.Mode != "self" && input.Review.Mode != "independent") {
return "", ErrInvalidInput
}
tasks := make([]workspaceservice.CreateIssueManagerTaskItemInput, 0, len(input.ActionableItems))
for _, item := range input.ActionableItems {
tasks = append(tasks, workspaceservice.CreateIssueManagerTaskItemInput{
TaskID: item.Task.ID,
Title: item.Task.Title,
Content: item.Task.Content,
Priority: item.Task.Priority,
AgentTargetID: item.Task.AgentTargetID,
ModelPlanID: item.Task.ModelPlanID,
Model: item.Task.Model,
PermissionModeID: item.Task.PermissionModeID,
ReasoningEffort: item.Task.ReasoningEffort,
ExecutionDirectory: item.Task.ExecutionDirectory,
DependencyTaskIDs: append([]string(nil), item.Task.DependsOn...),
Parallelizable: item.Task.Parallelizable,
AutoAccept: item.Task.AutoAccept,
})
}
detail, err := materializer.Issues.CreateIssueFromPlan(ctx, input.WorkspaceID, workspaceservice.CreateIssueManagerIssueFromPlanInput{
Issue: workspaceservice.CreateIssueManagerIssueInput{
IssueID: issueID,
TopicID: input.TopicID,
Title: input.Title,
Content: input.Content,
PlanningSource: string(workspaceissues.PlanningSourceTuttiModePlan),
SourceSessionID: input.SourceSessionID,
SequentialExecution: input.Execution.Mode == "sequential",
ParallelExecution: input.Execution.Mode == "parallel",
ExecutionProfile: workspaceissues.ExecutionProfile{
ReasoningIntensity: input.Execution.ReasoningIntensity,
OrchestrationIntensity: input.Execution.OrchestrationIntensity,
},
HasExecutionProfile: true,
Budget: workspaceissues.Budget{
Mode: workspaceissues.BudgetMode(input.Budget.Mode),
TokenLimit: input.Budget.TokenLimit,
QuotaWaterlinePercent: input.Budget.QuotaWaterlinePercent,
},
HasBudget: true,
TuttiModeWorkflowOwned: true,
TuttiModeWorkflowID: input.WorkflowID,
},
Tasks: tasks,
ReviewMode: input.Review.Mode,
ReviewAgentTargetID: input.Review.AgentTargetID,
})
if err == nil {
return detail.Issue.IssueID, nil
}
if !errors.Is(err, workspaceissues.ErrIssueAlreadyExists) {
return "", err
}
existing, getErr := materializer.Issues.GetIssueDetail(ctx, input.WorkspaceID, issueID)
if getErr != nil {
return "", getErr
}
if !materializedIssueMatches(existing, input, issueID) {
return "", fmt.Errorf("%w: %q", ErrIssueMaterializationConflict, issueID)
}
execution, executionErr := materializer.Issues.GetTuttiModeExecutionByIssue(ctx, input.WorkspaceID, issueID)
if executionErr != nil || !materializedExecutionMatches(execution, input, issueID) {
return "", fmt.Errorf("%w: %q", ErrIssueMaterializationConflict, issueID)
}
return existing.Issue.IssueID, nil
}
func materializedExecutionMatches(
aggregate executionbiz.Aggregate,
input MaterializeIssueInput,
issueID string,
) bool {
execution := aggregate.Execution
expectedExecutionID, executionIDOK := executionbiz.ExecutionID(issueID)
expectedCheckpointID, checkpointIDOK := executionbiz.InitialCheckpointID(expectedExecutionID)
if execution.WorkspaceID != strings.TrimSpace(input.WorkspaceID) ||
!executionIDOK ||
!checkpointIDOK ||
execution.ID != expectedExecutionID ||
execution.IssueID != issueID ||
execution.WorkflowID != strings.TrimSpace(input.WorkflowID) ||
execution.SourceSessionID != strings.TrimSpace(input.SourceSessionID) ||
execution.ReviewMode != executionbiz.ReviewMode(input.Review.Mode) ||
execution.ReviewAgentTargetID != strings.TrimSpace(input.Review.AgentTargetID) ||
!executionbiz.IsStatus(execution.Status) ||
execution.GraphRevision < 1 {
return false
}
for _, checkpoint := range aggregate.Checkpoints {
if checkpoint.ID == expectedCheckpointID &&
checkpoint.ExecutionID == execution.ID &&
checkpoint.Kind == executionbiz.CheckpointKindInitialSchedule &&
executionbiz.IsCheckpointStatus(checkpoint.Status) &&
checkpoint.Sequence == 1 &&
checkpoint.GraphRevision == 1 {
return true
}
}
return false
}
func materializedIssueMatches(existing workspaceissues.IssueDetail, input MaterializeIssueInput, issueID string) bool {
issue := existing.Issue
if strings.TrimSpace(issue.IssueID) != issueID ||
issue.PlanningSource != workspaceissues.PlanningSourceTuttiModePlan ||
strings.TrimSpace(issue.SourceSessionID) != strings.TrimSpace(input.SourceSessionID) ||
strings.TrimSpace(issue.TopicID) != strings.TrimSpace(input.TopicID) ||
strings.TrimSpace(issue.Title) != strings.TrimSpace(input.Title) ||
strings.TrimSpace(issue.Content) != strings.TrimSpace(input.Content) ||
issue.SequentialExecution != (input.Execution.Mode == "sequential") ||
issue.ParallelExecution != (input.Execution.Mode == "parallel") ||
issue.ExecutionProfile.ReasoningIntensity != input.Execution.ReasoningIntensity ||
issue.ExecutionProfile.OrchestrationIntensity != input.Execution.OrchestrationIntensity ||
issue.Budget.Mode != workspaceissues.BudgetMode(input.Budget.Mode) ||
issue.Budget.QuotaWaterlinePercent != input.Budget.QuotaWaterlinePercent ||
issue.Budget.Mode == workspaceissues.BudgetModeFixed && issue.Budget.TokenLimit != input.Budget.TokenLimit {
return false
}
if len(existing.Tasks) != len(input.ActionableItems) {
return false
}
existingByID := make(map[string]workspaceissues.Task, len(existing.Tasks))
for _, task := range existing.Tasks {
taskID := strings.TrimSpace(task.TaskID)
if taskID == "" {
return false
}
existingByID[taskID] = task
}
if len(existingByID) != len(existing.Tasks) {
return false
}
expectedIDs := make(map[string]struct{}, len(input.ActionableItems))
for index, item := range input.ActionableItems {
if item.Ordinal != index+1 {
return false
}
expected := item.Task
expectedID := strings.TrimSpace(expected.ID)
if expectedID == "" {
return false
}
if _, duplicate := expectedIDs[expectedID]; duplicate {
return false
}
expectedIDs[expectedID] = struct{}{}
actual, found := existingByID[expectedID]
if !found ||
actual.SortIndex != item.Ordinal ||
strings.TrimSpace(actual.Title) != strings.TrimSpace(expected.Title) ||
strings.TrimSpace(actual.Content) != strings.TrimSpace(expected.Content) ||
actual.Priority != workspaceissues.NormalizePriority(expected.Priority) ||
strings.TrimSpace(actual.AgentTargetID) != strings.TrimSpace(expected.AgentTargetID) ||
strings.TrimSpace(actual.ModelPlanID) != strings.TrimSpace(expected.ModelPlanID) ||
strings.TrimSpace(actual.Model) != strings.TrimSpace(expected.Model) ||
strings.TrimSpace(actual.PermissionModeID) != strings.TrimSpace(expected.PermissionModeID) ||
strings.TrimSpace(actual.ReasoningEffort) != strings.TrimSpace(expected.ReasoningEffort) ||
strings.TrimSpace(actual.ExecutionDirectory) != strings.TrimSpace(expected.ExecutionDirectory) ||
actual.Parallelizable != expected.Parallelizable ||
actual.AutoAccept != expected.AutoAccept ||
!slices.Equal(actual.DependencyTaskIDs, workspaceissues.NormalizeDependencyTaskIDs(expected.DependsOn)) {
return false
}
}
return true
}
func actionableItemsHaveCanonicalOrder(items []ActionableItem) bool {
for index, item := range items {
if item.Ordinal != index+1 {
return false
}
}
return true
}