fe7a5c481e
Working memory (Memory module's first real code): - MemoryEntry (schema "memory", vector(384), InitialMemory migration); TeamMemory implements the SharedKernel ITeamMemory seam (embed-and-store on write, cosine recall on read); GET /api/memory/search. HashingTextEmbedder promoted to SharedKernel (pure, deterministic; swapped for ONNX/BYOK embedders later behind ITextEmbedder). - Written on approval: Governance's approve stores an Approval/Correction entry per decision. - Read at assembly: the executor recalls the team's top-3 relevant entries; the prompt gains a "# Team memory" section (treated as data, not instructions). The single V1 event trigger: - IAgentDispatcher (SharedKernel) implemented by Assembler's AgentRunDispatcher (shared by the API and triggers). OrgBoard's QaHandoffTrigger: a task hitting done creates a QA task (provenance parent, assigned to the QA agent) and dispatches a run for the team's QA AI seat. Guardrails: Test/Review tasks never re-trigger (no self-cascade) and a task hands off at most once. Audited as handoff.triggered. Analytics — the V1 verdict view: - IBoardStats (SharedKernel) implemented by OrgBoard; GET /api/governance/analytics returns approval rate, avg edit distance, per-agent metrics + edit-distance trend, tasks done. - UI: /analytics — stat cards, per-agent table, recharts edit-distance trend per agent. Verified: build green; ArchitectureTests 8/8; IntegrationTests 42/42 incl. the M6 acceptance end to end — a dev marks a story done → Quill wakes via the handoff (QA task with provenance, assigned to the agent) → drafts a test plan that waits in review → approve records the second agent's edit distance → analytics show approval rate 100%, avg edit distance > 0, and trends for BOTH Aria and Quill; memory written on Aria's corrected approval is recalled into her next prompt; the guardrails hold. Client build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.9 KiB
C#
78 lines
2.9 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using TeamUp.Modules.OrgBoard.Domain;
|
|
using TeamUp.Modules.OrgBoard.Persistence;
|
|
using TeamUp.SharedKernel.Ai;
|
|
using TeamUp.SharedKernel.Auditing;
|
|
|
|
namespace TeamUp.Modules.OrgBoard.Runtime;
|
|
|
|
/// <summary>
|
|
/// The single V1 event trigger: a task hitting <c>done</c> emits a handoff that creates a QA task
|
|
/// (with provenance) for the team's QA AI seat and dispatches a run — the boundary is a pipe, not
|
|
/// a gate; the QA agent then acts per its OWN autonomy. Guardrails: QA/Review tasks never
|
|
/// re-trigger (no self-cascade), and a task hands off at most once (the duplicate check is the
|
|
/// V1 rate limit). The richer event mesh is Phase 1+.
|
|
/// </summary>
|
|
internal sealed class QaHandoffTrigger(
|
|
OrgBoardDbContext db,
|
|
IAgentDispatcher dispatcher,
|
|
IAuditLog audit,
|
|
TimeProvider clock,
|
|
ILogger<QaHandoffTrigger> logger)
|
|
{
|
|
private const string QaSkillKey = "test-plan-generation";
|
|
|
|
public async Task OnTaskDoneAsync(WorkItem item, Guid actorMemberId, CancellationToken cancellationToken = default)
|
|
{
|
|
// No self-cascade: QA's own output never wakes QA again.
|
|
if (item.Type is WorkItemType.Test or WorkItemType.Review)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// At most one handoff per task.
|
|
if (await db.WorkItems.AnyAsync(w => w.ParentId == item.Id && w.Type == WorkItemType.Test, cancellationToken))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The receiving seat: an AI seat on this team equipped with the QA skill.
|
|
var seat = await (
|
|
from s in db.Seats
|
|
join a in db.Agents on s.Id equals a.SeatId
|
|
where s.TeamId == item.TeamId && s.State == SeatState.Ai && a.SkillKeys.Contains(QaSkillKey)
|
|
orderby s.CreatedAtUtc
|
|
select s).FirstOrDefaultAsync(cancellationToken);
|
|
if (seat is null)
|
|
{
|
|
return; // no QA AI seat — nothing to hand off to
|
|
}
|
|
|
|
var now = clock.GetUtcNow();
|
|
var qaTask = new WorkItem(
|
|
item.TeamId,
|
|
"QA: " + item.Title,
|
|
"Handoff: \"" + item.Title + "\" hit done. Draft the test plan.",
|
|
WorkItemType.Test,
|
|
actorMemberId,
|
|
now,
|
|
parentId: item.Id);
|
|
if (seat.AgentId is { } agentId)
|
|
{
|
|
qaTask.AssignToAgent(agentId, now);
|
|
}
|
|
|
|
db.WorkItems.Add(qaTask);
|
|
await db.SaveChangesAsync(cancellationToken);
|
|
|
|
var runId = await dispatcher.DispatchAsync(seat.Id, qaTask.Id, cancellationToken);
|
|
await audit.WriteAsync(
|
|
new AuditEvent("handoff.triggered", "WorkItem", qaTask.Id, actorMemberId,
|
|
$"\"{item.Title}\" done → QA run {runId}"),
|
|
cancellationToken);
|
|
logger.LogInformation(
|
|
"PO→QA handoff: task {TaskId} done → QA task {QaTaskId}, run {RunId}.", item.Id, qaTask.Id, runId);
|
|
}
|
|
}
|