diff options
Diffstat (limited to 'src/Agent.Planner')
| -rw-r--r-- | src/Agent.Planner/Agent.Planner.csproj | 1 | ||||
| -rw-r--r-- | src/Agent.Planner/AssemblyMarker.cs | 3 | ||||
| -rw-r--r-- | src/Agent.Planner/PlannerTask.cs | 18 | ||||
| -rw-r--r-- | src/Agent.Planner/PlannerTaskState.cs | 11 | ||||
| -rw-r--r-- | src/Agent.Planner/PlannerValidationException.cs | 3 | ||||
| -rw-r--r-- | src/Agent.Planner/TaskGraph.cs | 179 | ||||
| -rw-r--r-- | src/Agent.Planner/TokenBudgetClass.cs | 31 | ||||
| -rw-r--r-- | src/Agent.Planner/TokenEstimate.cs | 15 |
8 files changed, 261 insertions, 0 deletions
diff --git a/src/Agent.Planner/Agent.Planner.csproj b/src/Agent.Planner/Agent.Planner.csproj new file mode 100644 index 0000000..6b512ec --- /dev/null +++ b/src/Agent.Planner/Agent.Planner.csproj @@ -0,0 +1 @@ +<Project Sdk="Microsoft.NET.Sdk" /> diff --git a/src/Agent.Planner/AssemblyMarker.cs b/src/Agent.Planner/AssemblyMarker.cs new file mode 100644 index 0000000..e90784d --- /dev/null +++ b/src/Agent.Planner/AssemblyMarker.cs @@ -0,0 +1,3 @@ +namespace Agent.Planner; + +public static class AssemblyMarker; diff --git a/src/Agent.Planner/PlannerTask.cs b/src/Agent.Planner/PlannerTask.cs new file mode 100644 index 0000000..edc85c2 --- /dev/null +++ b/src/Agent.Planner/PlannerTask.cs @@ -0,0 +1,18 @@ +namespace Agent.Planner; + +public sealed record PlannerTask( + Guid Id, + string Description, + IReadOnlySet<Guid> Dependencies, + PlannerTaskState State, + TokenBudgetClass BudgetClass, + TokenEstimate Estimate, + long ActualInputTokens, + long ActualOutputTokens, + bool RequiresExternalEffect) +{ + public double ExpectedTokens => Estimate.ExpectedTokens; + public long ActualTotalTokens => checked(ActualInputTokens + ActualOutputTokens); +} + +public sealed record CriticalPathResult(IReadOnlyList<Guid> TaskIds, double CumulativeTokenCost); diff --git a/src/Agent.Planner/PlannerTaskState.cs b/src/Agent.Planner/PlannerTaskState.cs new file mode 100644 index 0000000..d0b79bf --- /dev/null +++ b/src/Agent.Planner/PlannerTaskState.cs @@ -0,0 +1,11 @@ +namespace Agent.Planner; + +public enum PlannerTaskState +{ + Queued, + Planning, + Running, + Completed, + Failed, + Cancelled +} diff --git a/src/Agent.Planner/PlannerValidationException.cs b/src/Agent.Planner/PlannerValidationException.cs new file mode 100644 index 0000000..934d549 --- /dev/null +++ b/src/Agent.Planner/PlannerValidationException.cs @@ -0,0 +1,3 @@ +namespace Agent.Planner; + +public sealed class PlannerValidationException(string message) : InvalidOperationException(message); diff --git a/src/Agent.Planner/TaskGraph.cs b/src/Agent.Planner/TaskGraph.cs new file mode 100644 index 0000000..7a4d45b --- /dev/null +++ b/src/Agent.Planner/TaskGraph.cs @@ -0,0 +1,179 @@ +namespace Agent.Planner; + +public sealed class TaskGraph(int maxTasks = 32) +{ + private readonly Dictionary<Guid, MutableTask> _tasks = []; + private readonly List<Guid> _insertionOrder = []; + private CriticalPathResult _criticalPath = new([], 0); + + public int MaxTasks { get; } = maxTasks > 0 ? maxTasks : throw new ArgumentOutOfRangeException(nameof(maxTasks)); + public int Count => _tasks.Count; + public CriticalPathResult CriticalPath => _criticalPath; + public long TotalTokenBudget => _tasks.Values.Aggregate(0L, static (total, task) => checked(total + task.BudgetClass.MaxTokens())); + public IReadOnlyList<PlannerTask> Tasks => _insertionOrder.Select(id => _tasks[id].Snapshot()).ToArray(); + + public PlannerTask AddTask( + string description, + TokenBudgetClass budgetClass, + TokenEstimate estimate, + IEnumerable<Guid>? dependencies = null, + bool requiresExternalEffect = false, + Guid? id = null) + { + if (string.IsNullOrWhiteSpace(description)) throw new PlannerValidationException("Task description is required."); + if (_tasks.Count >= MaxTasks) throw new PlannerValidationException($"Task limit of {MaxTasks} exceeded."); + estimate.Validate(budgetClass); + Guid taskId = id ?? Guid.NewGuid(); + if (_tasks.ContainsKey(taskId)) throw new PlannerValidationException($"Task '{taskId}' already exists."); + HashSet<Guid> dependencySet = dependencies?.ToHashSet() ?? []; + EnsureDependenciesExist(dependencySet); + if (dependencySet.Contains(taskId)) throw new PlannerValidationException("A task cannot depend on itself."); + + _tasks.Add(taskId, new MutableTask(taskId, description, dependencySet, budgetClass, estimate, requiresExternalEffect)); + _insertionOrder.Add(taskId); + try + { + Recalculate(); + } + catch + { + _tasks.Remove(taskId); + _insertionOrder.Remove(taskId); + throw; + } + return _tasks[taskId].Snapshot(); + } + + public void AddDependency(Guid taskId, Guid dependencyId) + { + MutableTask task = GetMutable(taskId); + EnsureMutable(task); + _ = GetMutable(dependencyId); + if (!task.Dependencies.Add(dependencyId)) return; + try + { + Recalculate(); + } + catch + { + task.Dependencies.Remove(dependencyId); + throw; + } + } + + public void SetEstimate(Guid taskId, TokenBudgetClass budgetClass, TokenEstimate estimate) + { + MutableTask task = GetMutable(taskId); + EnsureMutable(task); + estimate.Validate(budgetClass); + (TokenBudgetClass previousBudget, TokenEstimate previousEstimate) = (task.BudgetClass, task.Estimate); + task.BudgetClass = budgetClass; + task.Estimate = estimate; + try { Recalculate(); } + catch + { + task.BudgetClass = previousBudget; + task.Estimate = previousEstimate; + throw; + } + } + + public void SetState(Guid taskId, PlannerTaskState state) + { + MutableTask task = GetMutable(taskId); + EnsureMutable(task); + task.State = state; + } + + public void AddUsage(Guid taskId, long inputTokens, long outputTokens) + { + if (inputTokens < 0 || outputTokens < 0) throw new ArgumentOutOfRangeException(nameof(inputTokens), "Token usage cannot be negative."); + MutableTask task = GetMutable(taskId); + EnsureMutable(task); + task.ActualInputTokens = checked(task.ActualInputTokens + inputTokens); + task.ActualOutputTokens = checked(task.ActualOutputTokens + outputTokens); + } + + public PlannerTask Get(Guid taskId) => GetMutable(taskId).Snapshot(); + + private void Recalculate() + { + IReadOnlyList<Guid> order = TopologicalOrder(); + var costs = new Dictionary<Guid, double>(); + var predecessors = new Dictionary<Guid, Guid?>(); + foreach (Guid id in order) + { + MutableTask task = _tasks[id]; + Guid? best = null; + double bestCost = 0; + foreach (Guid dependency in task.Dependencies.OrderBy(InsertionIndex)) + { + double candidate = costs[dependency]; + if (best is null || candidate > bestCost) + { + best = dependency; + bestCost = candidate; + } + } + costs[id] = bestCost + task.Estimate.ExpectedTokens; + predecessors[id] = best; + } + if (order.Count == 0) + { + _criticalPath = new([], 0); + return; + } + Guid end = order[0]; + foreach (Guid id in order.Skip(1)) if (costs[id] > costs[end]) end = id; + var path = new List<Guid>(); + for (Guid? current = end; current is not null; current = predecessors[current.Value]) path.Add(current.Value); + path.Reverse(); + _criticalPath = new(path, costs[end]); + } + + private IReadOnlyList<Guid> TopologicalOrder() + { + var indegree = _tasks.ToDictionary(static pair => pair.Key, static pair => pair.Value.Dependencies.Count); + var ready = new SortedSet<int>(_insertionOrder.Select((id, index) => (id, index)).Where(item => indegree[item.id] == 0).Select(static item => item.index)); + var result = new List<Guid>(_tasks.Count); + while (ready.Count > 0) + { + int index = ready.Min; + ready.Remove(index); + Guid id = _insertionOrder[index]; + result.Add(id); + foreach (Guid dependent in _insertionOrder.Where(candidate => _tasks[candidate].Dependencies.Contains(id))) + { + indegree[dependent]--; + if (indegree[dependent] == 0) ready.Add(InsertionIndex(dependent)); + } + } + if (result.Count != _tasks.Count) throw new PlannerValidationException("TaskGraph must be a DAG; the update creates a cycle."); + return result; + } + + private int InsertionIndex(Guid id) => _insertionOrder.IndexOf(id); + private MutableTask GetMutable(Guid id) => _tasks.TryGetValue(id, out MutableTask? task) ? task : throw new KeyNotFoundException($"Task '{id}' was not found."); + private void EnsureDependenciesExist(IEnumerable<Guid> dependencies) + { + foreach (Guid dependency in dependencies) _ = GetMutable(dependency); + } + private static void EnsureMutable(MutableTask task) + { + if (task.State == PlannerTaskState.Completed) throw new PlannerValidationException("Completed tasks are immutable."); + } + + private sealed class MutableTask(Guid id, string description, HashSet<Guid> dependencies, TokenBudgetClass budgetClass, TokenEstimate estimate, bool requiresExternalEffect) + { + public Guid Id { get; } = id; + public string Description { get; } = description; + public HashSet<Guid> Dependencies { get; } = dependencies; + public PlannerTaskState State { get; set; } = PlannerTaskState.Queued; + public TokenBudgetClass BudgetClass { get; set; } = budgetClass; + public TokenEstimate Estimate { get; set; } = estimate; + public long ActualInputTokens { get; set; } + public long ActualOutputTokens { get; set; } + public bool RequiresExternalEffect { get; } = requiresExternalEffect; + public PlannerTask Snapshot() => new(Id, Description, Dependencies.ToHashSet(), State, BudgetClass, Estimate, ActualInputTokens, ActualOutputTokens, RequiresExternalEffect); + } +} diff --git a/src/Agent.Planner/TokenBudgetClass.cs b/src/Agent.Planner/TokenBudgetClass.cs new file mode 100644 index 0000000..35894c7 --- /dev/null +++ b/src/Agent.Planner/TokenBudgetClass.cs @@ -0,0 +1,31 @@ +namespace Agent.Planner; + +public enum TokenBudgetClass +{ + K1 = 1_024, + K2 = 2_048, + K4 = 4_096, + K8 = 8_192, + K16 = 16_384, + K32 = 32_768, + K64 = 65_536, + K128 = 131_072 +} + +public static class TokenBudgetClassExtensions +{ + public static long MaxTokens(this TokenBudgetClass value) => (long)value; + + public static TokenBudgetClass Parse(string value) => value switch + { + "1K" => TokenBudgetClass.K1, + "2K" => TokenBudgetClass.K2, + "4K" => TokenBudgetClass.K4, + "8K" => TokenBudgetClass.K8, + "16K" => TokenBudgetClass.K16, + "32K" => TokenBudgetClass.K32, + "64K" => TokenBudgetClass.K64, + "128K" => TokenBudgetClass.K128, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown token budget class.") + }; +} diff --git a/src/Agent.Planner/TokenEstimate.cs b/src/Agent.Planner/TokenEstimate.cs new file mode 100644 index 0000000..4b8920a --- /dev/null +++ b/src/Agent.Planner/TokenEstimate.cs @@ -0,0 +1,15 @@ +namespace Agent.Planner; + +public readonly record struct TokenEstimate(long Optimistic, long MostLikely, long Pessimistic) +{ + public double ExpectedTokens => (Optimistic + (4d * MostLikely) + Pessimistic) / 6d; + public double VarianceTokens => Math.Pow((Pessimistic - Optimistic) / 6d, 2); + + public void Validate(TokenBudgetClass budgetClass) + { + if (Optimistic <= 0 || Optimistic > MostLikely || MostLikely > Pessimistic || Pessimistic > budgetClass.MaxTokens()) + { + throw new PlannerValidationException("Estimate must satisfy 0 < O <= M <= P <= token budget."); + } + } +} |
