summaryrefslogtreecommitdiff
path: root/src/Agent.Core/Tasks/TaskRegistry.cs
blob: b61e525f6141c9523462cf2c6a6927a42a864d67 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
using System.Collections.Concurrent;
using System.Threading.Channels;
using Agent.Core.Sessions;

namespace Agent.Core.Tasks;

public sealed class TaskRegistry
{
    private readonly ConcurrentDictionary<Guid, MutableTask> _tasks = new();
    private readonly SessionStore _sessions;
    private readonly Channel<Guid> _queue = Channel.CreateUnbounded<Guid>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });

    public TaskRegistry(SessionStore? sessions = null) => _sessions = sessions ?? new SessionStore();

    public SessionStore Sessions => _sessions;

    public TaskSnapshot Enqueue(string request, Guid? requestedSessionId, double? temperature, IReadOnlyList<string>? images = null)
    {
        if (string.IsNullOrWhiteSpace(request)) throw new ArgumentException("Request cannot be empty.", nameof(request));
        if (temperature is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(temperature));
        Guid sessionId;
        if (requestedSessionId is { } existing)
        {
            if (!_sessions.TryGet(existing, out _)) throw new KeyNotFoundException($"Session '{existing}' was not found.");
            sessionId = existing;
        }
        else
        {
            sessionId = _sessions.Create().SessionId;
        }

        var task = new MutableTask(Guid.NewGuid(), sessionId, request, temperature, images ?? []);
        if (!_tasks.TryAdd(task.TaskId, task)) throw new InvalidOperationException("Could not register task.");
        if (!_queue.Writer.TryWrite(task.TaskId))
        {
            _tasks.TryRemove(task.TaskId, out _);
            throw new InvalidOperationException("Task queue is not accepting work.");
        }
        return task.Snapshot();
    }

    public bool TryGet(Guid taskId, Guid? sessionId, out TaskSnapshot? snapshot)
    {
        if (_tasks.TryGetValue(taskId, out MutableTask? task) && (sessionId is null || task.SessionId == sessionId))
        {
            snapshot = task.Snapshot();
            return true;
        }
        snapshot = null;
        return false;
    }

    public IAsyncEnumerable<Guid> ReadQueuedAsync(CancellationToken cancellationToken) => _queue.Reader.ReadAllAsync(cancellationToken);

    public bool Transition(Guid taskId, AgentTaskState state, string? currentTask = null, string? result = null)
    {
        return _tasks.TryGetValue(taskId, out MutableTask? task) && task.Transition(state, currentTask, result);
    }

    public bool AddUsage(Guid taskId, long inputTokens, long outputTokens) =>
        _tasks.TryGetValue(taskId, out MutableTask? task) && task.AddUsage(inputTokens, outputTokens);

    private sealed class MutableTask(Guid taskId, Guid sessionId, string request, double? temperature, IReadOnlyList<string> images)
    {
        private readonly object _sync = new();
        private AgentTaskState _state = AgentTaskState.Queued;
        private string? _currentTask;
        private string? _result;
        private long _inputTokens;
        private long _outputTokens;

        public Guid TaskId { get; } = taskId;
        public Guid SessionId { get; } = sessionId;

        public TaskSnapshot Snapshot()
        {
            lock (_sync)
            {
                int completed = _state == AgentTaskState.Completed ? 1 : 0;
                return new(TaskId, SessionId, _state, request, temperature, _currentTask, _result, completed, 1, _inputTokens, _outputTokens, 0, images);
            }
        }

        public bool AddUsage(long inputTokens, long outputTokens)
        {
            if (inputTokens < 0 || outputTokens < 0) return false;
            lock (_sync)
            {
                _inputTokens += inputTokens;
                _outputTokens += outputTokens;
                return true;
            }
        }

        public bool Transition(AgentTaskState next, string? currentTask, string? result)
        {
            lock (_sync)
            {
                if (!Allowed(_state, next)) return false;
                _state = next;
                _currentTask = currentTask;
                _result = result;
                return true;
            }
        }

        private static bool Allowed(AgentTaskState current, AgentTaskState next) => current switch
        {
            AgentTaskState.Queued => next is AgentTaskState.Planning or AgentTaskState.Failed or AgentTaskState.Cancelled,
            AgentTaskState.Planning => next is AgentTaskState.Running or AgentTaskState.Failed or AgentTaskState.Cancelled,
            AgentTaskState.Running => next is AgentTaskState.Planning or AgentTaskState.Completed or AgentTaskState.Failed or AgentTaskState.Cancelled,
            _ => false
        };
    }
}