M4: the assembler — assemble → model → parse (Increment 2)

SharedKernel contracts (so Assembler stays decoupled): IAgentRunContextProvider (agent +
task) and ISkillCatalog (skill prompts by key). Implemented by OrgBoard (AgentRunContextProvider)
and Skills (SkillCatalog).

Assembler:
- PromptAssembler builds house-style + identity + the agent's skill bodies + the task, and
  derives the primary action + risk from the agent's first skill. RAG/working-memory join at M6.
- AgentRunExecutor (real): resolve context + skills → assemble → resolve BYOK config (with
  fallback) → call IModelClient → parse into action + risk → capture all on the AgentRun.

Verified: build green; ArchitectureTests 8/8; IntegrationTests 29/29 — incl. the M4 acceptance:
assigning a Spec task to Aria (PO, gated, stub BYOK) yields a Completed run with the assembled
prompt (skill body + task title), action "write-spec", risk "Draft", and model output. Nothing
executes — the gate is M5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-10 06:19:02 +03:30
parent 09eaf360a3
commit d9f9349117
10 changed files with 388 additions and 16 deletions
@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using TeamUp.Modules.Skills.Domain;
using TeamUp.Modules.Skills.Persistence;
using TeamUp.SharedKernel.Ai;
namespace TeamUp.Modules.Skills.Catalog;
/// <summary>Resolves skill prompts by key (latest version) for the assembler.</summary>
internal sealed class SkillCatalog(SkillsDbContext db) : ISkillCatalog
{
public async Task<IReadOnlyList<SkillPrompt>> GetByKeysAsync(
IReadOnlyCollection<string> keys,
CancellationToken cancellationToken = default)
{
if (keys.Count == 0)
{
return [];
}
var wanted = keys.ToHashSet();
var skills = await db.Skills.Where(s => wanted.Contains(s.SkillKey)).ToListAsync(cancellationToken);
return skills
.GroupBy(s => s.SkillKey)
.Select(group => group.OrderByDescending(s => s.Version, StringComparer.Ordinal).First())
.Select(s =>
{
var primary = s.Actions.Count > 0 ? s.Actions[0] : null;
return new SkillPrompt(
s.SkillKey,
s.Name,
s.Body,
primary?.Name ?? "respond",
(primary?.Risk ?? ActionRisk.Draft).ToString(),
s.Roles);
})
.ToList();
}
}
@@ -3,10 +3,12 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using TeamUp.Modules.Skills.Catalog;
using TeamUp.Modules.Skills.Endpoints;
using TeamUp.Modules.Skills.Indexing;
using TeamUp.Modules.Skills.Persistence;
using TeamUp.Modules.Skills.Sync;
using TeamUp.SharedKernel.Ai;
using TeamUp.SharedKernel.Modularity;
using TeamUp.SharedKernel.Persistence;
@@ -27,6 +29,7 @@ public sealed class SkillsModule : IModule
services.AddSingleton<ISkillEmbedder, HashingSkillEmbedder>();
services.AddScoped<SkillIndexer>();
services.AddScoped<SkillSyncService>();
services.AddScoped<ISkillCatalog, SkillCatalog>();
services.TryAddSingleton(TimeProvider.System);
}