diff --git a/docs/LAB.md b/docs/LAB.md index 300c0d5..270c9eb 100644 --- a/docs/LAB.md +++ b/docs/LAB.md @@ -103,6 +103,10 @@ machine's own git credentials. Every other request (spec generation, the 5-agent answer-only. It is still code from a ticket running on your box; the machine's owner can refuse scoped runs with `REMOTE_AGENT_AUTONOMY=off`. With the runner paired, `RUN_AGENTOS_E2E_REAL=true AGENTOS_REAL_URL=https://lab.dev.localhost:6180 AGENTOS_E2E_RUN_TIMEOUT_SECONDS=1200 dotnet test --project tests/AgentOs.E2E.Tests --filter-class "*WorkflowAppRealAuthTests"` runs the full 5-agent pipeline live. +The runner keeps two lanes: work runs (scoped: clone, build, push — `REMOTE_AGENT_MAX_PARALLEL`, default 2) and +answer-only runs such as a New idea spec or ticket split (`REMOTE_AGENT_MAX_ANSWERS`, default 2), so a person waiting on +the screen never queues behind Autopilot's coding. Specialist answers get a 5-minute deadline (`LlmRequest.AgentAnswerTimeout`). + ### Try the app on the lab With the runner paired, Board → VNChessAcademy → **Try the app** on a repository runs its latest default branch on diff --git a/src/AgentOs.Domain/Llm/LlmRequest.cs b/src/AgentOs.Domain/Llm/LlmRequest.cs index 1a602f4..49efc94 100644 --- a/src/AgentOs.Domain/Llm/LlmRequest.cs +++ b/src/AgentOs.Domain/Llm/LlmRequest.cs @@ -44,6 +44,12 @@ public sealed record LlmRequest( System.TimeSpan? Timeout = null, string? Cli = null) { + /// + /// Deadline for one specialist answer (a spec, a ticket split, a review) on a paired computer: a CLI answer takes a + /// minute or two, more on a busy machine. Above the 120 s dispatch default, far below a coding run's budget. + /// + public static readonly System.TimeSpan AgentAnswerTimeout = System.TimeSpan.FromMinutes(5); + /// /// Optional canonical agent role behind this request ("Requirement"/"Coding"/"Testing"/ /// "Qa"), set by the specialist agents. Lets a provider identify the agent WITHOUT parsing the system diff --git a/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs b/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs index 3ed911e..c047803 100644 --- a/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs +++ b/src/AgentOs.Modules.Pipeline/Agents/LlmAgentBase.cs @@ -112,6 +112,7 @@ protected async Task ExecuteAsync( // Carry the fixed agent role out-of-band so providers (notably the offline one) can identify the // agent without parsing the system prompt — robust against per-tenant prompt overrides. AgentRole = PromptKey, + Timeout = LlmRequest.AgentAnswerTimeout, }; var response = await llm.SendAsync(request, ct).ConfigureAwait(false); diff --git a/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs b/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs index dc49be5..d13ae84 100644 --- a/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs +++ b/src/AgentOs.Modules.Pipeline/Decomposition/IdeaBootstrapService.cs @@ -16,7 +16,12 @@ namespace AgentOs.Modules.Pipeline.Decomposition; /// A generated requirement spec plus the tickets decomposed from it — the wizard's preview. /// The structured requirement the idea produced. /// The right-sized tickets to create (still editable before the user confirms). -public sealed record BootstrapPreview(RequirementSpec Spec, IReadOnlyList Tickets); +public sealed record BootstrapPreview(RequirementSpec Spec, IReadOnlyList Tickets) +{ + /// False when the Decomposition agent couldn't refine the tickets (unreachable, timed out, unreadable answer) + /// and these are the deterministic drafts — one per requirement line. The wizard says so and offers to try again. + public bool Refined { get; init; } = true; +} /// Turns a free-text idea into a reviewable spec + ticket set (no side effects). public interface IIdeaBootstrapService @@ -48,6 +53,7 @@ public async Task GenerateAsync(string idea, string locale = " var seed = TicketMapper.Map(spec); var tickets = await _decomposer.RunAsync(spec, seed, idea, cancellationToken).ConfigureAwait(false); - return new BootstrapPreview(spec, tickets); + // The decomposer hands back the very seed list when it couldn't refine it. + return new BootstrapPreview(spec, tickets) { Refined = !ReferenceEquals(tickets, seed) }; } } diff --git a/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs b/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs index 453d988..6756416 100644 --- a/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs +++ b/src/AgentOs.Modules.Pipeline/Decomposition/TicketDecomposerAgent.cs @@ -78,7 +78,8 @@ public async Task> RunAsync(RequirementSpec spec, IRe UserPrompt: DecomposerPrompt.RenderUser(spec, seed, idea), Model: profile.Model, Temperature: profile.Temperature, - MaxTokens: profile.MaxTokens); + MaxTokens: profile.MaxTokens, + Timeout: LlmRequest.AgentAnswerTimeout); try { diff --git a/src/AgentOs.RemoteAgent/Program.cs b/src/AgentOs.RemoteAgent/Program.cs index 766982a..820fa0f 100644 --- a/src/AgentOs.RemoteAgent/Program.cs +++ b/src/AgentOs.RemoteAgent/Program.cs @@ -29,7 +29,9 @@ // REMOTE_AGENT_ALLOW shell allowlist for server-driven tool calls (see RunnerCommandPolicy): a comma list // replaces the defaults, "+name" adds, "*" allows everything (explicit opt-out) // REMOTE_AGENT_TIMEOUT_SECONDS per shell command (default 600); on expiry the whole process tree is killed -// REMOTE_AGENT_MAX_PARALLEL CLI runs at once on this machine (default 2); more wait their turn +// REMOTE_AGENT_MAX_PARALLEL work runs (scoped: clone, build, push) at once on this machine (default 2); more wait +// REMOTE_AGENT_MAX_ANSWERS answer-only runs (a spec, a ticket split — someone is waiting on screen) at once +// (default 2); their own lane, so they never queue behind a long work run // REMOTE_AGENT_DATA test data the agents may read: "NAME=C:\path;OTHER=/data/x" → env vars of each scoped run, // names (not paths) told to the agent, read-only to it (see TestData in ScopedAutonomy.cs) // REMOTE_AGENT_PREVIEWS "off" refuses "Try the app" previews on this machine (default: allowed — the member clicks @@ -107,14 +109,20 @@ // So handlers only enqueue; the work runs off the receive loop, at most REMOTE_AGENT_MAX_PARALLEL CLI runs at once. var maxParallel = int.TryParse(Environment.GetEnvironmentVariable("REMOTE_AGENT_MAX_PARALLEL"), out var mp) && mp > 0 ? mp : 2; var cliSlots = new SemaphoreSlim(maxParallel, maxParallel); +// Answer-only requests (no tools on this machine) are short and usually have a person waiting on them. Behind two +// 15-minute coding runs they outlived the server's deadline — the ticket split fell back to the raw draft list. +var maxAnswers = int.TryParse(Environment.GetEnvironmentVariable("REMOTE_AGENT_MAX_ANSWERS"), out var ma) && ma > 0 ? ma : 2; +var answerSlots = new SemaphoreSlim(maxAnswers, maxAnswers); // M3 — full-prompt LLM dispatch connection.On("Execute", request => { Console.WriteLine($"[agent] Execute {request.Id} (model={request.Model})"); + var lane = AgentOs.RemoteAgent.ScopedAutonomy.Grants(request.Autonomy, Environment.GetEnvironmentVariable("REMOTE_AGENT_AUTONOMY")) + ? cliSlots : answerSlots; _ = Task.Run(async () => { - await cliSlots.WaitAsync().ConfigureAwait(false); + await lane.WaitAsync().ConfigureAwait(false); try { // Stopped while it waited for a slot: never start it. @@ -135,7 +143,7 @@ } finally { - cliSlots.Release(); + lane.Release(); } }); return Task.CompletedTask; diff --git a/src/AgentOs.Web/Components/Pages/BoardApp.razor b/src/AgentOs.Web/Components/Pages/BoardApp.razor index b28c084..c316c8c 100644 --- a/src/AgentOs.Web/Components/Pages/BoardApp.razor +++ b/src/AgentOs.Web/Components/Pages/BoardApp.razor @@ -461,6 +461,18 @@ else if (_tab == Tab.Idea)

@_preview.Spec.Title

@_preview.Spec.Summary

+ @if (!_preview.Refined) + { +
+ The AI couldn't split this into tickets this time. + These are rough drafts, one per requirement line — try again (your computer may have been busy), or edit and remove rows below. +
+ + @(_ideaBusy ? "Trying…" : "Try again") + +
+
+ } diff --git a/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs b/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs index b32b2bc..bcd396a 100644 --- a/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs +++ b/tests/AgentOs.Tests/Decomposition/IdeaBootstrapServiceTests.cs @@ -36,6 +36,24 @@ await requirement.Received(1).RunAsync( // The decomposer is fed the deterministic seed (non-empty) built from the spec. await decomposer.Received(1).RunAsync( spec, Arg.Is>(seed => seed!.Count > 0), "build a product catalog", Arg.Any()); + preview.Refined.ShouldBeTrue(); + } + + [Fact] + public async Task GenerateAsync_DecomposerFellBackToSeed_IsNotRefined() + { + var spec = DecompositionFixtures.Spec(); + var requirement = Substitute.For(); + requirement.RunAsync(Arg.Any(), Arg.Any()).Returns(spec); + var decomposer = Substitute.For(); + // The real agent returns the very seed list it was given when it can't refine it. + decomposer.RunAsync(spec, Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(ci => ci.ArgAt>(1)); + + var preview = await new IdeaBootstrapService(requirement, decomposer).GenerateAsync("build a product catalog"); + + preview.Refined.ShouldBeFalse(); + preview.Tickets.ShouldNotBeEmpty(); } [Fact]