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

425 lines
13 KiB
Go

package workspaceissues
import (
"math"
"slices"
"strings"
)
// MaxWorkspaceParallelRuns is the shared admission ceiling for Issue task
// Runs, regardless of whether the generic dispatcher or Tutti execution
// admits them.
const MaxWorkspaceParallelRuns = 4
const (
DefaultReasoningIntensity = 50
DefaultOrchestrationIntensity = 50
DefaultQuotaWaterlinePercent = 10
minimumAutoTokenBudget = int64(32_000)
maximumAutoTokenBudget = int64(2_000_000)
)
func NormalizeStatus(raw string) (Status, bool) {
switch Status(strings.ToLower(strings.TrimSpace(raw))) {
case StatusNotStarted, StatusRunning, StatusPendingAcceptance, StatusCompleted, StatusFailed, StatusCanceled:
return Status(strings.ToLower(strings.TrimSpace(raw))), true
default:
return "", false
}
}
func NormalizeRunCompletionStatus(raw string) (Status, bool) {
switch Status(strings.ToLower(strings.TrimSpace(raw))) {
case StatusCompleted, StatusFailed, StatusCanceled:
return Status(strings.ToLower(strings.TrimSpace(raw))), true
default:
return "", false
}
}
func NormalizePriority(raw string) Priority {
switch Priority(strings.ToLower(strings.TrimSpace(raw))) {
case PriorityHigh, PriorityMedium, PriorityLow:
return Priority(strings.ToLower(strings.TrimSpace(raw)))
default:
return PriorityMedium
}
}
func NormalizePlanningSource(raw string) (PlanningSource, bool) {
switch PlanningSource(strings.ToLower(strings.TrimSpace(raw))) {
case PlanningSourceManual, PlanningSourceTuttiModePlan, PlanningSourceTraditionalPlan:
return PlanningSource(strings.ToLower(strings.TrimSpace(raw))), true
default:
return "", false
}
}
func DefaultExecutionProfile() ExecutionProfile {
return ExecutionProfile{
ReasoningIntensity: DefaultReasoningIntensity,
OrchestrationIntensity: DefaultOrchestrationIntensity,
}
}
func NormalizeExecutionProfile(profile ExecutionProfile) (ExecutionProfile, bool) {
if profile.ReasoningIntensity < 0 || profile.ReasoningIntensity > 100 ||
profile.OrchestrationIntensity < 0 || profile.OrchestrationIntensity > 100 {
return ExecutionProfile{}, false
}
return profile, true
}
func DefaultBudget() Budget {
return Budget{
Mode: BudgetModeAuto,
QuotaWaterlinePercent: DefaultQuotaWaterlinePercent,
Status: BudgetStatusActive,
}
}
func NormalizeBudget(value Budget) (Budget, bool) {
value.Mode = BudgetMode(strings.ToLower(strings.TrimSpace(string(value.Mode))))
if value.Mode == "" {
value.Mode = BudgetModeAuto
}
if value.Status == "" {
value.Status = BudgetStatusActive
} else {
value.Status = BudgetStatus(strings.ToLower(strings.TrimSpace(string(value.Status))))
}
if value.TokenLimit < 0 || value.ConsumedTokens < 0 || !finitePercentage(value.QuotaWaterlinePercent) ||
!finiteNumber(value.RemainingQuotaPercent) ||
value.HasRemainingQuota && !finitePercentage(value.RemainingQuotaPercent) {
return Budget{}, false
}
switch value.Mode {
case BudgetModeAuto:
// The compiler owns the effective limit in auto mode.
value.TokenLimit = maxInt64(value.TokenLimit, 0)
case BudgetModeFixed:
if value.TokenLimit <= 0 {
return Budget{}, false
}
default:
return Budget{}, false
}
switch value.Status {
case BudgetStatusActive, BudgetStatusSoftLimited:
default:
return Budget{}, false
}
return value, true
}
func finitePercentage(value float64) bool {
return finiteNumber(value) && value >= 0 && value <= 100
}
func finiteNumber(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
func CompileAutoTokenBudget(taskCount int, profile ExecutionProfile) int64 {
if taskCount < 1 {
taskCount = 1
}
compiled := int64(16_000) + int64(taskCount)*CompileEstimatedRunTokenBudget(profile)
if compiled < minimumAutoTokenBudget {
return minimumAutoTokenBudget
}
if compiled > maximumAutoTokenBudget {
return maximumAutoTokenBudget
}
return compiled
}
// CompileEstimatedRunTokenBudget returns the conservative allowance reserved
// before one automatic Issue task is dispatched. Keeping this compiler aligned
// with CompileAutoTokenBudget lets lowering the Issue intensities make a
// previously blocked remaining task fit without silently increasing a fixed
// token limit.
func CompileEstimatedRunTokenBudget(profile ExecutionProfile) int64 {
return 24_000 + int64(profile.ReasoningIntensity)*320 + int64(profile.OrchestrationIntensity)*160
}
// IssueBudgetAllowsNextAutomaticRun applies the pre-dispatch soft gate. It
// reserves an estimated allowance instead of waiting until actual usage has
// already crossed the limit, which makes the documented lower-intensity
// recovery path meaningful.
func IssueBudgetAllowsNextAutomaticRun(issue Issue) bool {
if issue.Budget.Status != BudgetStatusActive {
return false
}
if issue.Budget.HasRemainingQuota && issue.Budget.RemainingQuotaPercent <= issue.Budget.QuotaWaterlinePercent {
return false
}
if issue.Budget.TokenLimit <= 0 {
return true
}
remaining := issue.Budget.TokenLimit - issue.Budget.ConsumedTokens
return remaining >= CompileEstimatedRunTokenBudget(issue.ExecutionProfile)
}
// IssueAutomaticBudgetSlots reserves one estimated allowance for every
// already-active Run before admitting more work for the same Issue.
func IssueAutomaticBudgetSlots(issue Issue, activeRunCount int) int {
if !IssueBudgetAllowsNextAutomaticRun(issue) {
return 0
}
if issue.Budget.TokenLimit <= 0 {
return MaxWorkspaceParallelRuns
}
allowance := CompileEstimatedRunTokenBudget(issue.ExecutionProfile)
remaining := issue.Budget.TokenLimit - issue.Budget.ConsumedTokens
if allowance <= 0 || remaining < allowance {
return 0
}
slots := int(remaining/allowance) - activeRunCount
if slots < 0 {
return 0
}
return slots
}
// IssueAutomaticRunAdmissionSlots is the shared concurrency and budget policy
// for every automatic Issue Run admission path. Persistence adapters supply
// authoritative counts, then compare-and-set the selected Runs atomically.
func IssueAutomaticRunAdmissionSlots(issue Issue, workspaceActiveRunCount int, issueActiveRunCount int) int {
workspaceSlots := MaxWorkspaceParallelRuns - workspaceActiveRunCount
if workspaceSlots < 0 {
workspaceSlots = 0
}
return min(workspaceSlots, IssueAutomaticBudgetSlots(issue, issueActiveRunCount))
}
type TaskRunBlocker string
const (
TaskRunBlockerSuperseded TaskRunBlocker = "task_superseded"
TaskRunBlockerNotStarted TaskRunBlocker = "task_not_started"
TaskRunBlockerMissingAgentTarget TaskRunBlocker = "missing_agent_target"
TaskRunBlockerDependencyUnsatisfied TaskRunBlocker = "dependency_unsatisfied"
)
// IssueTaskRunBlocker is the shared task/dependency predicate used by generic
// dispatch, source-Agent exact-set scheduling, and execution diagnostics.
func IssueTaskRunBlocker(task Task, tasksByID map[string]Task) TaskRunBlocker {
if task.IsSuperseded() {
return TaskRunBlockerSuperseded
}
if task.Status != StatusNotStarted {
return TaskRunBlockerNotStarted
}
if strings.TrimSpace(task.AgentTargetID) == "" {
return TaskRunBlockerMissingAgentTarget
}
for _, dependencyID := range task.DependencyTaskIDs {
dependency, ok := tasksByID[dependencyID]
if !ok || dependency.IsSuperseded() ||
dependency.Status != StatusCompleted ||
dependency.AcceptanceState != AcceptanceUserAccepted {
return TaskRunBlockerDependencyUnsatisfied
}
}
return ""
}
// IssueTaskEligibleForRun preserves the boolean admission interface for
// existing dispatch callers while diagnostics retain the exact blocker.
func IssueTaskEligibleForRun(task Task, tasksByID map[string]Task) bool {
return IssueTaskRunBlocker(task, tasksByID) == ""
}
// ProjectReviewedSettlementTaskForRun applies the acceptance fact that an
// exact-set schedule commits when it advances from a task_settled checkpoint.
// The returned copy is for admission/readiness evaluation only.
func ProjectReviewedSettlementTaskForRun(task Task) Task {
if task.Status == StatusPendingAcceptance {
task.Status = StatusCompleted
task.AcceptanceState = AcceptanceUserAccepted
}
return task
}
func IssueRunClientSubmitID(runID string) string {
runID = strings.TrimSpace(runID)
if runID == "" {
return ""
}
return "issue-run:" + runID
}
// CompileAutoTokenBudgetWithHistory blends the deterministic scale/intensity
// compiler with the observed total usage of comparable completed tasks. The
// deterministic result remains the fallback and contributes half the result,
// so one unusual historical run cannot wholly redefine a new Issue budget.
func CompileAutoTokenBudgetWithHistory(taskCount int, profile ExecutionProfile, historicalTotalTokens int64) int64 {
compiled := CompileAutoTokenBudget(taskCount, profile)
if historicalTotalTokens <= 0 {
return compiled
}
historicalWithHeadroom := historicalTotalTokens + historicalTotalTokens/4
if historicalWithHeadroom < minimumAutoTokenBudget {
historicalWithHeadroom = minimumAutoTokenBudget
}
if historicalWithHeadroom > maximumAutoTokenBudget {
historicalWithHeadroom = maximumAutoTokenBudget
}
blended := compiled/2 + historicalWithHeadroom/2
if blended < minimumAutoTokenBudget {
return minimumAutoTokenBudget
}
if blended > maximumAutoTokenBudget {
return maximumAutoTokenBudget
}
return blended
}
func NormalizeAcceptanceState(raw string) (AcceptanceState, bool) {
switch AcceptanceState(strings.ToLower(strings.TrimSpace(raw))) {
case AcceptanceAgentClaimed, AcceptanceAutoChecked, AcceptanceUserAccepted:
return AcceptanceState(strings.ToLower(strings.TrimSpace(raw))), true
default:
return "", false
}
}
func CanTransitionAcceptance(current AcceptanceState, next AcceptanceState) bool {
if current == next {
return true
}
switch current {
case AcceptanceAgentClaimed:
return next == AcceptanceAutoChecked || next == AcceptanceUserAccepted
case AcceptanceAutoChecked:
return next == AcceptanceUserAccepted
default:
return false
}
}
func NormalizeDependencyTaskIDs(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func ValidateTaskDependencyGraph(tasks []Task) bool {
byID := make(map[string]Task, len(tasks))
for _, task := range tasks {
if task.TaskID == "" {
return false
}
byID[task.TaskID] = task
}
visiting := make(map[string]bool, len(tasks))
visited := make(map[string]bool, len(tasks))
var visit func(string) bool
visit = func(taskID string) bool {
if visiting[taskID] {
return false
}
if visited[taskID] {
return true
}
task, exists := byID[taskID]
if !exists {
return false
}
visiting[taskID] = true
for _, dependencyID := range task.DependencyTaskIDs {
if dependencyID == taskID || !visit(dependencyID) {
return false
}
}
visiting[taskID] = false
visited[taskID] = true
return true
}
ids := make([]string, 0, len(byID))
for taskID := range byID {
ids = append(ids, taskID)
}
slices.Sort(ids)
for _, taskID := range ids {
if !visit(taskID) {
return false
}
}
return true
}
func TaskStatusForCompletedRun(status Status) Status {
if status == StatusCompleted {
return StatusPendingAcceptance
}
return status
}
func ProjectIssueStatus(counts StatusCounts) Status {
switch {
case counts.Running > 0:
return StatusRunning
case counts.Failed > 0:
return StatusFailed
case counts.All == 0 || counts.NotStarted == counts.All:
return StatusNotStarted
case counts.All > 0 && counts.Completed == counts.All:
return StatusCompleted
case counts.All > 0 && counts.PendingAcceptance > 0 && counts.NotStarted == 0 && counts.Running == 0 && counts.Failed == 0:
return StatusPendingAcceptance
case counts.All > 0 && counts.Canceled == counts.All:
return StatusCanceled
default:
return StatusRunning
}
}
func NormalizeContextRefParentKind(raw string) (ContextRefParentKind, bool) {
switch ContextRefParentKind(strings.ToLower(strings.TrimSpace(raw))) {
case ContextRefParentIssue, ContextRefParentTask:
return ContextRefParentKind(strings.ToLower(strings.TrimSpace(raw))), true
default:
return "", false
}
}
func TrimSearchText(content string) string {
return strings.TrimSpace(content)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
func NormalizeTokenUsage(value TokenUsage) (TokenUsage, bool) {
if value.InputTokens < 0 || value.OutputTokens < 0 || value.CacheReadTokens < 0 || value.CacheWriteTokens < 0 {
return TokenUsage{}, false
}
return value, true
}
func maxInt(left int, right int) int {
if left > right {
return left
}
return right
}