M4: agent-run job queue + worker drain (Increment 1)

SharedKernel: IWorkerModule seam (RegisterWorker runs in the worker host only).
Bootstrap: AddTeamUpWorkerServices; the worker host now wires it.

Assembler module (schema "assembler", InitialAssembler migration):
- Job (Pending→Processing→Done/Failed) + AgentRun (Queued→Running→Completed/Failed) entities.
- JobQueue: enqueue + ClaimNextAsync using `FOR UPDATE SKIP LOCKED` in a transaction.
- AgentRunExecutor (Increment-1 placeholder — real assemble/model/parse lands in Increment 2).
- JobProcessor BackgroundService drains the queue on the worker host (web off the model path).
- POST /api/assembler/runs enqueues a run; GET /api/assembler/runs/{id} reads it.

Verified: build green; ArchitectureTests 8/8 (Assembler references only SharedKernel);
IntegrationTests 28/28 incl. enqueue→claim(SKIP LOCKED)→process→Completed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-10 01:16:37 +03:30
parent 34ea407e86
commit 09eaf360a3
18 changed files with 906 additions and 17 deletions
@@ -0,0 +1,72 @@
using TeamUp.SharedKernel.Domain;
namespace TeamUp.Modules.Assembler.Domain;
internal enum AgentRunStatus
{
Queued,
Running,
Completed,
Failed,
}
/// <summary>
/// One execution of an AI seat against a task: the assembled prompt, the raw model output, the
/// parsed action + risk tag, and the reasoning/assembly trace. Nothing executes off this in M4 —
/// the action gate (M5) decides whether the parsed action runs or waits in review.
/// </summary>
internal sealed class AgentRun : Entity
{
public Guid SeatId { get; private set; }
public Guid WorkItemId { get; private set; }
public Guid? AgentId { get; private set; }
public AgentRunStatus Status { get; private set; }
public string? Prompt { get; private set; }
public string? Output { get; private set; }
public string? ActionType { get; private set; }
public string? ActionRisk { get; private set; }
public string? ResultJson { get; private set; }
public string? Trace { get; private set; }
public string? Error { get; private set; }
public long? LatencyMs { get; private set; }
public DateTimeOffset CreatedAtUtc { get; private set; }
public DateTimeOffset? CompletedAtUtc { get; private set; }
private AgentRun()
{
}
public AgentRun(Guid seatId, Guid workItemId, DateTimeOffset createdAtUtc)
{
SeatId = seatId;
WorkItemId = workItemId;
Status = AgentRunStatus.Queued;
CreatedAtUtc = createdAtUtc;
}
public void Start(Guid? agentId, string prompt, string? trace)
{
Status = AgentRunStatus.Running;
AgentId = agentId;
Prompt = prompt;
Trace = trace;
}
public void Complete(string output, string actionType, string actionRisk, string? resultJson, long latencyMs, DateTimeOffset nowUtc)
{
Status = AgentRunStatus.Completed;
Output = output;
ActionType = actionType;
ActionRisk = actionRisk;
ResultJson = resultJson;
LatencyMs = latencyMs;
CompletedAtUtc = nowUtc;
}
public void Fail(string error, DateTimeOffset nowUtc)
{
Status = AgentRunStatus.Failed;
Error = error;
CompletedAtUtc = nowUtc;
}
}