Skip to content

feat(agents): every Board agent is configured from Agent profiles and Prompts - #278

Open
hoangsnowy wants to merge 4 commits into
mainfrom
feat/one-agent-registry
Open

hoangsnowy wants to merge 4 commits into
mainfrom
feat/one-agent-registry

Conversation

@hoangsnowy

@hoangsnowy hoangsnowy commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Change description

One agent registry. Every agent that runs in the Board / Autopilot flow is now configured from Settings → Agent profiles and Settings → Prompts exactly like the pipeline agents, shows up in Insights under the same name, reads the project practices and passes the budget gate. Before this PR the UI implied all of that but the runtime ignored it: four Board agents hard-coded factory.Create("RemoteAgent") and Model: "claude-sonnet-4-6", and the paired runner never passed the model to the CLI.

Role keys (AgentRoles). Each key is used for the profile (Agents:<Key>:*), the prompt (prompt/<Key>/system), the AgentRole of LLM calls (what Insights groups by) and the run label:

Shown as Key Seed (keeps today's behaviour)
Discovery (New idea: requirement + ticket split) Discovery Anthropic · Computer's default · 4000
Coding (tickets) IssueWork Anthropic · Computer's default · 32768
Reviewer Reviewer RemoteAgent · Computer's default · 32768
Testing (tickets) Acceptance RemoteAgent · Computer's default · 32768
Autopilot lead AutopilotOrchestrator RemoteAgent · Computer's default · 4096
Retrospective Retrospective RemoteAgent · Computer's default · 4096
Orchestrator / Requirement / Coding / Testing / QA (Workflow pipeline) Orchestrator Requirement Coding Testing Qa unchanged

What changed:

  1. Profiles per call. ReviewAgent, AcceptanceAgent, AutopilotOrchestratorAgent and RetrospectiveAgent resolve their profile through IAgentProfiles (BoardAgent.ProfileAsync) with seeds in AgentsOptions. IssueWork and the decomposer use the registry keys too.
  2. Agent profiles UI. There are two tables now: Board agents and Workflow pipeline agents. Each row shows the label, one line on what the agent does and its source badge. Pipeline labels say "(Workflow pipeline)". Rows carry data-role="<key>". Discovery replaces the Decomposer row. New idea's requirement step runs the Requirement agent under an AgentRunScope("Discovery"), so it uses the Discovery profile and is filed as Discovery, while keeping the Requirement prompt and spec contract.
  3. Computer's default, and a chosen model reaches the CLI. Board agents choose no model by default, shown as "Computer's default" in Agent profiles. This keeps today's behaviour, where the member's CLI ran its own, newer, default model; pinning claude-sonnet-4-6 would have downgraded it.
    • On a member's computer: the request carries an empty model. RemoteAgentLlmClient accepts it via the new LlmRequest.ValidateAllowingNoModel, the runner adds no flag, and metrics name it computer-default.
    • On our servers: the provider client fills in its configured model before Validate (ProviderDefaultModel). That is Claude's Llm:Claude:Model, the Azure default deployment, the vendor's or the OpenAI-compatible server's model. It is wired into PooledChatLlmClient for every provider, MafChatClient and ChatClientLlmClient. LlmClientFactory, Offline and Failover are untouched.
    • A model someone picked is passed: --model <id> for claude, -m <id> for codex (CliModel). Nothing is added when that CLI can't serve the model (a GPT id on claude), for an unknown CLI, or when REMOTE_AGENT_ARGS already names a model. The id is validated so it can never become a flag. The runner log shows model=<id|cli default>.
    • Startup validation still requires a model for the Workflow-pipeline agents.
  4. Prompts. PromptCatalog lists every Board agent, grouped like Agent profiles: Discovery, Coding (tickets) on a member's computer (IssueWork), Coding (tickets) on the server (IssueWork.Server), Reviewer, Testing (tickets), Autopilot lead and Retrospective. Board prompts are templates. Their {owner}, {repo}, {branch}… placeholders are filled per run in a single pass (PromptTemplate), and the UI lists them. The untrusted-text security rule is appended by code and can't be overridden. Workspace rules apply to all of them. The first lines stay "You are the Agent …"; the IssueWork openings are unchanged.
  5. Practices. AcceptanceAgent and AutopilotOrchestratorAgent read ProjectMemory.PracticesBlock through the shared ProjectPractices helper, which the Reviewer now uses too. The Autopilot lead gets the board's repositories from a new Domain IBoardRepos, implemented in Workspaces. The KnowledgeTab copy now names the agents that actually read practices.
  6. Budget gate (AgentBudgetGate) applies to every Board agent and to Discovery. It is decided from the effective provider (client.Provider after force-provider). Only a call that really runs on a member's computer is exempt. IssueWork now derives CLI mode from the effective provider too, so a forced server provider can't skip the gate.
  7. Insights groups by AgentRoles.Canonical and shows the Agent profiles label. Board agents keep their own keys (IssueWork and Acceptance are no longer merged into pipeline Coding and Testing). Run labels come from the registry. The mascot maps the Board keys.
  8. Guard. AgentRegistryGuardTests parses every file under Modules.Pipeline/Agents and /Decomposition with Roslyn (syntax only). It fails on a string literal, an interpolated string or a same-file const passed as the provider to ILlmClientFactory.Create or as an LlmRequest Model, whether named or positional, new LlmRequest(…) or target-typed new(…). Theory cases prove the scanner flags each form. This adds a test-only reference to Microsoft.CodeAnalysis.CSharp 5.9.0, the latest stable on nuget.org today.

Docs: docs/CONFIG.md (Agent profiles section rewritten: keys, seeds, model-to-CLI, budget gate, Discovery, prompt placeholders, practices) and README.md (Concepts, default profiles, "Add a pipeline agent").

Decomposer → Discovery, without losing saved values. LegacyAgentSettings reads Agents:Decomposer:<Field> for any Discovery field with nothing stored. It checks the tenant first, then the platform, then appsettings. The Decomposer prompt override, its history and the Prompts page fall back the same way until a Discovery override is saved. In Agent profiles the row shows Saved as "Decomposer" — still applies. Saving a field stores it under Discovery; Reset, for the profile or the prompt, also clears the tenant's Decomposer values. This is documented in docs/CONFIG.md.

Run labels for new Board runs are IssueWork and Acceptance. Older rows labelled Coding or Testing still group with the pipeline roles in Insights.

Claude Design: push pending. The new Agent profiles layout (two tables, "Computer's default") and the grouped Prompts page are only in the repo for now. They still have to be pushed to the Claude Design project by a user-run /design-sync; a non-interactive session can't authenticate DesignSync.

Not touched (owned by other changes): LlmClientFactory, LlmCallScope, OfflineLlmClient, FailoverLlmClient, AutopilotPlanner/AutopilotSettings/TicketWorkflow/ProjectOrchestrator*. ProjectOrchestrator.Agent.cs still saves the decision run labelled "Orchestrator"; Insights files its calls under AutopilotOrchestrator because the request now carries that AgentRole.

Type of change

  • New feature (new agent / endpoint / capability)
  • Bugfix
  • Refactor (no behavior change)
  • Documentation / README
  • Infrastructure / CI configuration

Related issues

Audit "one agent registry": hard-coded Board agent provider/model, model never reaching the CLI, Board prompts not tunable, practices and budget gate missing on Board agents, Insights role names.

Checklist

  • dotnet build passes locally in Release mode: 0 errors. No new warnings. Remaining warnings are pre-existing, in AgentOs.Web (TreatWarningsAsErrors=false): CA2016/CA1001 in untouched lines of BoardTaskService, BoardTicketSteps, ProjectOrchestrator.Agent.cs, plus ASPIRE010.
  • dotnet test --solution AgentOs.slnx -c Release: 1712 total, 1637 passed, 0 failed, 75 skipped (live/real-auth gated), after merging main (fix(llm): Board agent runs never get the offline demo in place of the real model #277).
  • No secrets committed
  • README / docs updated
  • Measurable results provided when related to agent output quality: n/a. No prompt text changed except placeholders; Insights now attributes Board calls to their own roles.

Test plan

  • Unit: BoardAgentRegistryTests covers:
    • the registry and seeds;
    • profile model and provider applied per call, with the AgentRole set;
    • a forced server provider is gated (Reviewer, Testing, Autopilot lead, Retrospective, IssueWork, Discovery);
    • member's-computer runs are exempt and never query the budget;
    • prompt overrides are filled per run, followed by the fixed rule and the workspace rules;
    • IssueWork picks the IssueWork or IssueWork.Server prompt by effective provider;
    • Testing and the Autopilot lead read practices, strictly per tenant;
    • Discovery's requirement step and split use the Discovery profile and role, while the Requirement agent outside it keeps its own.
  • Unit: CliModelTests (claude --model, codex -m, skip cases, operator override, Windows paths on Linux) and AgentRegistryGuardTests.
  • Unit: ComputersDefaultModelTests:
    • (a) a default Board profile on RemoteAgent sends an empty model to the runner, and CliModel.Flags returns [];
    • (b) the same agent on a server provider runs the provider's configured model and passes Validate;
    • (c) an explicitly chosen model reaches the runner and becomes --model <id>;
    • a server client with no model anywhere still fails validation;
    • RemoteAgent metrics name the model computer-default;
    • Board seeds are empty and pass startup validation, while pipeline agents still need a model;
    • Decomposer → Discovery legacy profile values (tenant store, appsettings seed, strictly per tenant, cleared by Reset) and the legacy prompt override (applied, shown in Prompts, cleared by Reset, a Discovery save wins).
  • E2E, standalone Web (ASPNETCORE_ENVIRONMENT=Development dotnet run --project src/AgentOs.Web, DevAutoLogin): the full AgentOs.E2E.Tests suite ran 50 tests, 30 passed and 20 skipped (real-auth). AgentProfilesTests was updated: two tables, the Reviewer row is present, the QA row is found by data-role="Qa" / "QA (Workflow pipeline) model".
  • Playwright screenshots, standalone Web, light and dark, captured locally and not committed:
    • Agent profiles · Board agents: Discovery, Coding (tickets), Reviewer, Testing (tickets), Autopilot lead, Retrospective. Each has a one-line description and a Default badge. Model "Computer's default", with the hint "Empty = the computer's own model (the member's CLI default). On our servers: the provider's model from Settings → Model keys"; "via Claude" for Discovery and Coding (tickets), "via RemoteAgent" for the other four. Re-captured after the review fix, and the E2E suite was re-run green (50 tests: 30 passed, 20 skipped).
    • Agent profiles · Workflow pipeline agents: Orchestrator, Requirement, Coding, Testing and QA, each "(Workflow pipeline)", with their unchanged models (haiku-4-5 / sonnet-4-6 / gpt-4.1 / gpt-4o-mini / haiku-4-5). Below them, the "Workflow pipeline · QA loop" panel.
    • Prompts: a "Board agents" heading with Discovery — New idea → tickets, Coding (tickets) on a member's computer and on the server (with the placeholder list), Reviewer, Testing (tickets), Autopilot lead and Retrospective. Then a "Workflow pipeline agents" heading with Requirement, Coding, Testing and QA (Workflow pipeline). Every Board prompt shows its built-in text and a Default badge.
  • Not verified on the full Aspire stack or the lab: persistence of Board profile and prompt overrides per tenant, and a live paired-runner run showing model=<id>. Standalone shows only the degraded path.

🤖 Generated with Claude Code

https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB

hoangsnowy and others added 2 commits October 24, 2026 01:08
… Prompts

One agent registry (AgentRoles): every agent has one stable role key used for
its profile (Agents:<Key>:*), its prompt (Settings -> Prompts), the AgentRole
of its LLM calls (Insights "by agent") and its run label.

- Reviewer, Testing (tickets) = Acceptance, Autopilot lead =
  AutopilotOrchestrator and Retrospective no longer hard-code
  factory.Create("RemoteAgent") + "claude-sonnet-4-6": they resolve their
  profile per call (seeds keep RemoteAgent + claude-sonnet-4-6).
- Agent profiles shows the Board agents and the Workflow pipeline in two
  tables; pipeline labels say "(Workflow pipeline)". Discovery replaces the
  Decomposer profile/prompt and also drives New idea's requirement step
  (AgentRunScope).
- Settings -> Prompts lists every Board agent; Board prompts are templates
  with {placeholders} filled per run (PromptTemplate); the untrusted-text rule
  stays code-appended.
- Budget gate on every Board agent, decided from the EFFECTIVE provider
  (client.Provider after force-provider); only real member's-computer runs
  are exempt. IssueWork now derives CLI mode from it too.
- Testing (tickets) and the Autopilot lead read the project practices
  (ProjectPractices, IBoardRepos from Workspaces).
- The paired runner passes the profile model to the CLI: claude --model,
  codex -m (CliModel), only when that CLI can serve it.
- Insights groups by AgentRoles.Canonical and shows the Agent profiles label.
- Guard: AgentRegistryGuardTests parses Agents/ and Decomposition/ with Roslyn
  and fails on a literal provider to ILlmClientFactory.Create or a literal
  LlmRequest Model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
Comment on lines +73 to +76
foreach (var role in AgentRoles.All)
{
if (For(role.Key) is { } options) { yield return options; }
}
Comment on lines +78 to +87
foreach (var call in root.DescendantNodes().OfType<InvocationExpressionSyntax>())
{
if (call.Expression is MemberAccessExpressionSyntax { Name.Identifier.ValueText: "Create" }
&& call.ArgumentList.Arguments is [var provider, ..]
&& provider.NameColon is null or { Name.Identifier.ValueText: "providerName" }
&& HardCoded(provider.Expression))
{
yield return $"line {Line(call)}: ILlmClientFactory.Create({provider.Expression})";
}
}
… values keep working

Review of #278:

1. No model downgrade. Board agents (Discovery, Coding/Reviewer/Testing
   (tickets), Autopilot lead, Retrospective) now choose NO model by default
   ("Computer's default" in Agent profiles). On a member's computer the
   request carries an empty model (RemoteAgentLlmClient accepts it via
   LlmRequest.ValidateAllowingNoModel) and CliModel adds no flag, so the CLI
   runs its own default. On our servers the provider client fills in its
   configured model before Validate (ProviderDefaultModel, wired into
   PooledChatLlmClient per provider, MafChatClient, ChatClientLlmClient). An
   explicitly chosen model is still passed (--model / -m). Startup
   validation still requires a model for the Workflow pipeline agents.

2. No silent loss on the Decomposer -> Discovery rename. LegacyAgentSettings
   reads Agents:Decomposer:* (tenant, platform, appsettings) for any Discovery
   field with nothing stored, and the Decomposer prompt override applies
   until a Discovery one is saved. Agent profiles marks the row and Reset
   clears the legacy values too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
Comment on lines +232 to +239
foreach (var field in RoleFields.Where(f => byKey.GetValueOrDefault(AgentSettings.Key(role, f))?.Source is null or SettingSource.Default))
{
if (await Legacy.ReadAsync(role, field, _tenantId) is { } old)
{
row.Loaded[field] = old;
row.Legacy = true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants