Forge
csharpdeeb25a2
1using CascadeIDE.Cockpit.DataBus;
2using CascadeIDE.Models;
3using CascadeIDE.Services;
4using DotNetBuildTest.Core;
5
6namespace CascadeIDE.Features.Agent.Environment;
7
8public interface IAgentEnvironmentService
9{
10 AgentEnvironmentStatusSnapshot GetStatus();
11
12 AgentVerifyStartResult StartVerify(
13 string solutionPath,
14 AgentVerifyPolicy policy,
15 AgentSandboxProfile? sandboxProfile = null);
16
17 /// <summary>Batch entry (W6): optional worktree profile для long autonomous runs.</summary>
18 AgentVerifyStartResult StartVerifyBatch(AgentVerifyBatchRequest request);
19
20 AgentSandboxPrepareResult PrepareSandbox(AgentSandboxProfile profile, string? workspaceRoot = null);
21
22 bool CancelActive();
23
24 AgentEnvironmentLastRunSummary? GetLastRun();
25
26 AgentVerifyEpochTracker EpochTracker { get; }
27
28 IAgentOrchestrator Orchestrator { get; }
29
30 AgentAeeRoslynBridge RoslynBridge { get; }
31
32 void NotifyCideWindowFocus(bool focused);
33}
34
35/// <summary>Verify ladder + runner (ADR 0148 W1–W6).</summary>
36public sealed class AgentEnvironmentService : IAgentEnvironmentService
37{
38 private readonly IDataBus _dataBus;
39 private readonly AgentEnvironmentSettings _settings;
40 private readonly BuildTestJobCoordinator _coordinator;
41 private readonly IGitCommandRunner? _gitRunner;
42 private readonly Func<string?>? _getWorkspaceRootForSnapshot;
43 private IBuildTestHost _buildTestHost;
44 private VerificationLadder _ladder;
45 private readonly AgentSandboxManager _sandbox;
46 private readonly AgentWorktreeSandbox? _worktree;
47 private readonly AgentVerifyEpochTracker _epoch;
48 private readonly AgentRoslynDiagnoseFilesDiagnostics _diagnoseFiles;
49 private readonly AgentOrchestrator _orchestrator;
50 private readonly AgentIdleUserTracker _idleUser;
51 private readonly AgentAeeRoslynBridge _roslynBridge;
52 private readonly object _hostGate = new();
53 private readonly object _gate = new();
54 private AgentEnvironmentRun? _active;
55 private AgentEnvironmentLastRunSummary? _last;
56 private CancellationTokenSource? _activeCts;
57 private AgentSandboxLease? _activeLease;
58
59 public AgentEnvironmentService(
60 IDataBus dataBus,
61 AgentEnvironmentSettings settings,
62 BuildTestJobService? buildTestJobService = null,
63 CSharpLanguageService? languageService = null,
64 Func<IReadOnlyList<(string Path, string Content)>>? openCsDocuments = null,
65 IGitCommandRunner? gitRunner = null,
66 Func<string?>? getWorkspaceRootForSnapshot = null,
67 Func<string?>? getSolutionPathForOrchestrator = null,
68 Func<IReadOnlyList<string>>? getWarmupCsFilePaths = null)
69 {
70 _dataBus = dataBus;
71 _settings = settings;
72 _gitRunner = gitRunner;
73 _getWorkspaceRootForSnapshot = getWorkspaceRootForSnapshot;
74 _sandbox = new AgentSandboxManager();
75 _epoch = new AgentVerifyEpochTracker(dataBus);
76 _coordinator = buildTestJobService?.Coordinator ?? new BuildTestJobCoordinator();
77 _buildTestHost = BuildTestHostFactory.Create(settings, _coordinator);
78 _diagnoseFiles = new AgentRoslynDiagnoseFilesDiagnostics(
79 languageService,
80 openCsDocuments,
81 settings.Ladder,
82 gitRunner,
83 getWorkspaceRootForSnapshot,
84 getWarmupCsFilePaths);
85 _ladder = CreateLadder();
86 _worktree = gitRunner is null ? null : new AgentWorktreeSandbox(gitRunner);
87 _orchestrator = new AgentOrchestrator(
88 this,
89 settings,
90 getSolutionPathForOrchestrator ?? (() => null),
91 () => AgentVerifyPolicyParser.TryParse(_settings.DefaultVerifyPolicy, out var p)
92 ? p
93 : AgentVerifyPolicy.Standard);
94 _idleUser = new AgentIdleUserTracker();
95 _roslynBridge = new AgentAeeRoslynBridge(languageService);
96
97 _dataBus.Subscribe<AgentEnvironmentTaskDied>(_ => TryRestartBuildTestHostAfterDied());
98 }
99
100 public AgentVerifyEpochTracker EpochTracker => _epoch;
101
102 public IAgentOrchestrator Orchestrator => _orchestrator;
103
104 public AgentAeeRoslynBridge RoslynBridge => _roslynBridge;
105
106 public void NotifyCideWindowFocus(bool focused) => _idleUser.NotifyCideFocus(focused);
107
108 public AgentEnvironmentStatusSnapshot GetStatus()
109 {
110 lock (_gate)
111 {
112 if (_active is null)
113 return new AgentEnvironmentStatusSnapshot(false, null, null, null, null);
114
115 return new AgentEnvironmentStatusSnapshot(
116 true,
117 _active.RunId,
118 _active.VerifySnapshotId,
119 _active.PolicyWire,
120 _active.SandboxWire,
121 WritesInvalidatedVerifyEpoch: _epoch.WritesInvalidatedVerifyEpoch,
122 SandboxRunDirectory: _activeLease?.RunDirectory,
123 ExecutionChannel: _buildTestHost.HostKind,
124 SolutionPath: _active.SolutionPath);
125 }
126 }
127
128 public AgentSandboxPrepareResult PrepareSandbox(AgentSandboxProfile profile, string? workspaceRoot = null)
129 {
130 var runId = Guid.NewGuid().ToString("N");
131 try
132 {
133 if (profile == AgentSandboxProfile.AgentWorktree && _worktree is not null
134 && !string.IsNullOrWhiteSpace(workspaceRoot))
135 {
136 var wt = _worktree.TryCreateAsync(workspaceRoot, runId).GetAwaiter().GetResult();
137 if (!wt.Success)
138 return new(false, null, wt.Error);
139
140 var lease = _sandbox.Prepare(runId, profile, workspaceRoot);
141 return new(true, lease.RunDirectory, $"worktree: {wt.WorktreePath}");
142 }
143
144 var leaseOnly = _sandbox.Prepare(runId, profile, workspaceRoot);
145 return new(true, leaseOnly.RunDirectory, AgentSandboxProfileParser.ToWire(profile));
146 }
147 catch (Exception ex)
148 {
149 return new(false, null, ex.Message);
150 }
151 }
152
153 public AgentVerifyStartResult StartVerify(
154 string solutionPath,
155 AgentVerifyPolicy policy,
156 AgentSandboxProfile? sandboxProfile = null)
157 {
158 if (string.IsNullOrWhiteSpace(solutionPath) || !File.Exists(solutionPath))
159 return new AgentVerifyStartResult(false, null, null, "Solution path is missing or not found.");
160
161 var profile = sandboxProfile ?? ResolveDefaultSandboxProfile();
162
163 lock (_gate)
164 {
165 CancelActiveCore("superseded");
166 var runId = Guid.NewGuid().ToString("N");
167 var snapshotId = VerifySnapshot.Create(solutionPath, _gitRunner, _getWorkspaceRootForSnapshot?.Invoke());
168 _activeCts = new CancellationTokenSource();
169 _activeLease = _sandbox.Prepare(runId, profile);
170 _active = new AgentEnvironmentRun(
171 runId,
172 snapshotId,
173 AgentVerifyPolicyParser.ToWire(policy),
174 solutionPath,
175 AgentSandboxProfileParser.ToWire(profile));
176 _epoch.Begin(runId, snapshotId, solutionPath);
177 _epoch.WatchPath(solutionPath);
178 }
179
180 AgentEnvironmentRun run;
181 AgentSandboxLease lease;
182 CancellationToken ct;
183 lock (_gate)
184 {
185 run = _active!;
186 lease = _activeLease!;
187 ct = _activeCts!.Token;
188 }
189
190 _dataBus.Publish(new AgentRunStarted(
191 run.RunId,
192 run.VerifySnapshotId,
193 run.PolicyWire,
194 run.SolutionPath));
195 _dataBus.Publish(new AgentRunPhaseChanged(run.RunId, AgentRunPhaseKind.Environment));
196
197 _ = Task.Run(() => ExecuteVerifyAsync(run, policy, lease, ct), ct);
198
199 return new AgentVerifyStartResult(true, run.RunId, run.VerifySnapshotId, null);
200 }
201
202 public AgentVerifyStartResult StartVerifyBatch(AgentVerifyBatchRequest request)
203 {
204 if (string.IsNullOrWhiteSpace(request.SolutionPath))
205 return new(false, null, null, "solution_path is required for batch verify.");
206
207 var profile = request.UseWorktree
208 ? AgentSandboxProfile.AgentWorktree
209 : request.SandboxProfile;
210
211 return StartVerify(request.SolutionPath, request.Policy, profile);
212 }
213
214 public bool CancelActive()
215 {
216 lock (_gate)
217 return CancelActiveCore("cancel");
218 }
219
220 public AgentEnvironmentLastRunSummary? GetLastRun()
221 {
222 lock (_gate)
223 return _last;
224 }
225
226 private AgentSandboxProfile ResolveDefaultSandboxProfile()
227 {
228 return AgentSandboxProfileParser.TryParse(_settings.DefaultSandboxProfile, out var p)
229 ? p
230 : AgentSandboxProfile.AgentEphemeral;
231 }
232
233 private bool CancelActiveCore(string? staleReason)
234 {
235 if (_active is null)
236 return false;
237
238 var runId = _active.RunId;
239 _ladder.Runner.CancelJobsForRun(runId);
240
241 _activeCts?.Cancel();
242 if (staleReason is not null)
243 _epoch.End(staleReason);
244 else
245 _epoch.End();
246
247 _active = null;
248 _activeLease = null;
249 _activeCts?.Dispose();
250 _activeCts = null;
251 return true;
252 }
253
254 private async Task ExecuteVerifyAsync(
255 AgentEnvironmentRun run,
256 AgentVerifyPolicy policy,
257 AgentSandboxLease lease,
258 CancellationToken cancellationToken)
259 {
260 try
261 {
262 var threshold = _settings.TimeAccounting.IdleUserThresholdMs;
263 if (threshold > 0)
264 _idleUser.SampleWhileUnfocused(threshold);
265
266 var result = await _ladder.ClimbAsync(
267 run.RunId,
268 run.SolutionPath,
269 policy,
270 lease,
271 cancellationToken).ConfigureAwait(false);
272
273 var slices = result.Slices.ToList();
274 foreach (var idle in _idleUser.DrainSlices())
275 slices.Add(idle);
276
277 _dataBus.Publish(new AgentRunCompleted(
278 run.RunId,
279 result.Green,
280 result.MaxRungReached,
281 slices));
282
283 lock (_gate)
284 {
285 _last = new AgentEnvironmentLastRunSummary(
286 run.RunId,
287 run.VerifySnapshotId,
288 result.Green,
289 result.MaxRungReached,
290 slices,
291 DateTimeOffset.UtcNow);
292 if (_active?.RunId == run.RunId)
293 {
294 _active = null;
295 _activeLease = null;
296 _activeCts?.Dispose();
297 _activeCts = null;
298 _epoch.End();
299 }
300 }
301 }
302 catch (OperationCanceledException)
303 {
304 _dataBus.Publish(new AgentRunCompleted(run.RunId, false, "cancelled", []));
305 lock (_gate)
306 {
307 if (_active?.RunId == run.RunId)
308 CancelActiveCore("cancel");
309 }
310 }
311 }
312
313 private VerificationLadder CreateLadder() =>
314 new(
315 _dataBus,
316 _buildTestHost,
317 _diagnoseFiles,
318 _settings,
319 _sandbox,
320 _gitRunner,
321 _getWorkspaceRootForSnapshot);
322
323 private void TryRestartBuildTestHostAfterDied()
324 {
325 if (!IsSupervisedWorkerHost(_settings.BuildVerifyHost))
326 return;
327
328 lock (_hostGate)
329 {
330 _buildTestHost.MarkUnhealthy();
331 _buildTestHost = BuildTestHostFactory.Create(_settings, _coordinator);
332 _ladder = CreateLadder();
333 _buildTestHost.MarkHealthy();
334 }
335 }
336
337 private static bool IsSupervisedWorkerHost(string? hostKind)
338 {
339 var h = hostKind?.Trim() ?? "";
340 return string.Equals(h, BuildTestHostFactory.WorkerProcessHostKind, StringComparison.OrdinalIgnoreCase)
341 || string.Equals(h, BuildTestHostFactory.WorkerDaemonHostKind, StringComparison.OrdinalIgnoreCase);
342 }
343}
344
345internal sealed record AgentEnvironmentRun(
346 string RunId,
347 string VerifySnapshotId,
348 string PolicyWire,
349 string SolutionPath,
350 string SandboxWire);
351
352public sealed record AgentVerifyStartResult(
353 bool Accepted,
354 string? RunId,
355 string? VerifySnapshotId,
356 string? Error);
357
358public sealed record AgentSandboxPrepareResult(
359 bool Success,
360 string? Path,
361 string? Detail);
362
363public sealed record AgentEnvironmentStatusSnapshot(
364 bool IsActive,
365 string? RunId,
366 string? VerifySnapshotId,
367 string? Policy,
368 string? SandboxProfile,
369 bool WritesInvalidatedVerifyEpoch = false,
370 string? SandboxRunDirectory = null,
371 string ExecutionChannel = "supervised-inproc",
372 string? SolutionPath = null)
373{
374 public AgentEnvironmentStatusSnapshot(bool isActive, string? runId, string? verifySnapshotId, string? policy)
375 : this(isActive, runId, verifySnapshotId, policy, null, false, null, "supervised-inproc")
376 {
377 }
378
379 public AgentEnvironmentStatusSnapshot(bool isActive, string? runId, string? verifySnapshotId, string? policy, string? sandboxProfile)
380 : this(isActive, runId, verifySnapshotId, policy, sandboxProfile, false, null, "supervised-inproc")
381 {
382 }
383}
384
385public sealed record AgentEnvironmentLastRunSummary(
386 string RunId,
387 string VerifySnapshotId,
388 bool Green,
389 string MaxRungReached,
390 IReadOnlyList<AgentTimeSlice> TimeSlices,
391 DateTimeOffset CompletedAtUtc)
392{
393 public string FormatChatTrace()
394 {
395 var envLine = TimeSlices.FirstOrDefault(s => s.Phase == AgentRunPhaseKind.Environment);
396 var envText = envLine is null
397 ? "—"
398 : $"{envLine.DurationSeconds:0.0}s ({envLine.Detail ?? "environment"})";
399
400 return $"""
401 Agent verify {RunId[..8]}…
402 Environment: {envText}
403 Policy snapshot: {VerifySnapshotId}
404 Status: {(Green ? "green" : "failed")} ({MaxRungReached})
405 """;
406 }
407}
408
View only · write via MCP/CIDE