Skip to content

Commit 3fdeb0e

Browse files
authored
Merge pull request #75 from managedcode/codex/issue-23-communication-contracts
Adopt ManagedCode.Communication runtime contracts for issue #23
2 parents 3b90575 + 15c553d commit 3fdeb0e

15 files changed

+476
-45
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<PackageVersion Include="NUnit" Version="4.5.1" />
1616
<PackageVersion Include="NUnit3TestAdapter" Version="6.1.0" />
1717
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.1" />
18+
<PackageVersion Include="ManagedCode.Communication" Version="10.0.1" />
1819
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
1920
<PackageVersion Include="Uno.UITest.Helpers" Version="1.1.0-dev.70" />
2021
<PackageVersion Include="Xamarin.UITest" Version="4.3.4" />

DotPilot.Core/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Stack: `.NET 10`, class library, feature-aligned contracts and provider-independ
1313
- `DotPilot.Core.csproj`
1414
- `Features/ApplicationShell/AppConfig.cs`
1515
- `Features/ControlPlaneDomain/*`
16+
- `Features/RuntimeCommunication/*`
1617
- `Features/RuntimeFoundation/*`
1718

1819
## Boundaries

DotPilot.Core/DotPilot.Core.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@
66
<NoWarn>$(NoWarn);CS1591</NoWarn>
77
</PropertyGroup>
88

9+
<ItemGroup>
10+
<PackageReference Include="ManagedCode.Communication" />
11+
</ItemGroup>
12+
913
</Project>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace DotPilot.Core.Features.RuntimeCommunication;
2+
3+
public enum RuntimeCommunicationProblemCode
4+
{
5+
PromptRequired,
6+
ProviderUnavailable,
7+
ProviderAuthenticationRequired,
8+
ProviderMisconfigured,
9+
ProviderOutdated,
10+
RuntimeHostUnavailable,
11+
OrchestrationUnavailable,
12+
PolicyRejected,
13+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System.Globalization;
2+
using System.Net;
3+
using DotPilot.Core.Features.ControlPlaneDomain;
4+
using ManagedCode.Communication;
5+
6+
namespace DotPilot.Core.Features.RuntimeCommunication;
7+
8+
public static class RuntimeCommunicationProblems
9+
{
10+
private const string PromptField = "Prompt";
11+
private const string PromptRequiredDetail = "Prompt is required before the runtime can execute a turn.";
12+
private const string ProviderUnavailableFormat = "{0} is unavailable in the current environment.";
13+
private const string ProviderAuthenticationRequiredFormat = "{0} requires authentication before the runtime can execute a turn.";
14+
private const string ProviderMisconfiguredFormat = "{0} is misconfigured and cannot execute a runtime turn.";
15+
private const string ProviderOutdatedFormat = "{0} is outdated and must be updated before the runtime can execute a turn.";
16+
private const string RuntimeHostUnavailableDetail = "The embedded runtime host is unavailable for the requested operation.";
17+
private const string OrchestrationUnavailableDetail = "The orchestration runtime is unavailable for the requested operation.";
18+
private const string PolicyRejectedFormat = "The requested action was rejected by policy: {0}.";
19+
20+
public static Problem InvalidPrompt()
21+
{
22+
var problem = Problem.Create(
23+
RuntimeCommunicationProblemCode.PromptRequired,
24+
PromptRequiredDetail,
25+
(int)HttpStatusCode.BadRequest);
26+
27+
problem.AddValidationError(PromptField, PromptRequiredDetail);
28+
return problem;
29+
}
30+
31+
public static Problem ProviderUnavailable(ProviderConnectionStatus status, string providerDisplayName)
32+
{
33+
ArgumentException.ThrowIfNullOrWhiteSpace(providerDisplayName);
34+
35+
return status switch
36+
{
37+
ProviderConnectionStatus.Available => throw new ArgumentOutOfRangeException(nameof(status), status, "Available status does not map to a problem."),
38+
ProviderConnectionStatus.Unavailable => CreateProblem(
39+
RuntimeCommunicationProblemCode.ProviderUnavailable,
40+
ProviderUnavailableFormat,
41+
providerDisplayName,
42+
HttpStatusCode.ServiceUnavailable),
43+
ProviderConnectionStatus.RequiresAuthentication => CreateProblem(
44+
RuntimeCommunicationProblemCode.ProviderAuthenticationRequired,
45+
ProviderAuthenticationRequiredFormat,
46+
providerDisplayName,
47+
HttpStatusCode.Unauthorized),
48+
ProviderConnectionStatus.Misconfigured => CreateProblem(
49+
RuntimeCommunicationProblemCode.ProviderMisconfigured,
50+
ProviderMisconfiguredFormat,
51+
providerDisplayName,
52+
HttpStatusCode.FailedDependency),
53+
ProviderConnectionStatus.Outdated => CreateProblem(
54+
RuntimeCommunicationProblemCode.ProviderOutdated,
55+
ProviderOutdatedFormat,
56+
providerDisplayName,
57+
HttpStatusCode.PreconditionFailed),
58+
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown provider status."),
59+
};
60+
}
61+
62+
public static Problem RuntimeHostUnavailable()
63+
{
64+
return Problem.Create(
65+
RuntimeCommunicationProblemCode.RuntimeHostUnavailable,
66+
RuntimeHostUnavailableDetail,
67+
(int)HttpStatusCode.ServiceUnavailable);
68+
}
69+
70+
public static Problem OrchestrationUnavailable()
71+
{
72+
return Problem.Create(
73+
RuntimeCommunicationProblemCode.OrchestrationUnavailable,
74+
OrchestrationUnavailableDetail,
75+
(int)HttpStatusCode.ServiceUnavailable);
76+
}
77+
78+
public static Problem PolicyRejected(string policyName)
79+
{
80+
ArgumentException.ThrowIfNullOrWhiteSpace(policyName);
81+
82+
return CreateProblem(
83+
RuntimeCommunicationProblemCode.PolicyRejected,
84+
PolicyRejectedFormat,
85+
policyName,
86+
HttpStatusCode.Forbidden);
87+
}
88+
89+
private static Problem CreateProblem(
90+
RuntimeCommunicationProblemCode code,
91+
string detailFormat,
92+
string value,
93+
HttpStatusCode statusCode)
94+
{
95+
return Problem.Create(
96+
code,
97+
string.Format(CultureInfo.InvariantCulture, detailFormat, value),
98+
(int)statusCode);
99+
}
100+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using ManagedCode.Communication;
2+
13
namespace DotPilot.Core.Features.RuntimeFoundation;
24

35
public interface IAgentRuntimeClient
46
{
5-
ValueTask<AgentTurnResult> ExecuteAsync(AgentTurnRequest request, CancellationToken cancellationToken);
7+
ValueTask<Result<AgentTurnResult>> ExecuteAsync(AgentTurnRequest request, CancellationToken cancellationToken);
68
}

DotPilot.Core/Features/RuntimeFoundation/RuntimeFoundationContracts.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public sealed record AgentTurnRequest(
2121
SessionId SessionId,
2222
AgentProfileId AgentProfileId,
2323
string Prompt,
24-
AgentExecutionMode Mode);
24+
AgentExecutionMode Mode,
25+
ProviderConnectionStatus ProviderStatus = ProviderConnectionStatus.Available);
2526

2627
public sealed record AgentTurnResult(
2728
string Summary,

DotPilot.Runtime/Features/RuntimeFoundation/DeterministicAgentRuntimeClient.cs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
using System.Diagnostics;
21
using DotPilot.Core.Features.ControlPlaneDomain;
2+
using DotPilot.Core.Features.RuntimeCommunication;
33
using DotPilot.Core.Features.RuntimeFoundation;
4+
using ManagedCode.Communication;
45

56
namespace DotPilot.Runtime.Features.RuntimeFoundation;
67

78
public sealed class DeterministicAgentRuntimeClient : IAgentRuntimeClient
89
{
910
private const string ApprovalKeyword = "approval";
11+
private const string DeterministicProviderDisplayName = "Deterministic Runtime Client";
1012
private const string PlanSummary =
1113
"Planned the runtime foundation flow with contracts first, then communication, host lifecycle, and orchestration.";
1214
private const string ExecuteSummary =
@@ -19,34 +21,50 @@ public sealed class DeterministicAgentRuntimeClient : IAgentRuntimeClient
1921
private const string ExecuteArtifact = "runtime-foundation.snapshot.json";
2022
private const string ReviewArtifact = "runtime-foundation.review.md";
2123

22-
public ValueTask<AgentTurnResult> ExecuteAsync(AgentTurnRequest request, CancellationToken cancellationToken)
24+
public ValueTask<Result<AgentTurnResult>> ExecuteAsync(AgentTurnRequest request, CancellationToken cancellationToken)
2325
{
24-
ArgumentException.ThrowIfNullOrWhiteSpace(request.Prompt);
2526
cancellationToken.ThrowIfCancellationRequested();
27+
if (string.IsNullOrWhiteSpace(request.Prompt))
28+
{
29+
return ValueTask.FromResult(Result<AgentTurnResult>.Fail(RuntimeCommunicationProblems.InvalidPrompt()));
30+
}
31+
32+
if (request.ProviderStatus is not ProviderConnectionStatus.Available)
33+
{
34+
return ValueTask.FromResult(
35+
Result<AgentTurnResult>.Fail(
36+
RuntimeCommunicationProblems.ProviderUnavailable(
37+
request.ProviderStatus,
38+
DeterministicProviderDisplayName)));
39+
}
2640

2741
return ValueTask.FromResult(request.Mode switch
2842
{
29-
AgentExecutionMode.Plan => new AgentTurnResult(
30-
PlanSummary,
31-
SessionPhase.Plan,
32-
ApprovalState.NotRequired,
33-
[CreateArtifact(request.SessionId, PlanArtifact, ArtifactKind.Plan)]),
34-
AgentExecutionMode.Execute when RequiresApproval(request.Prompt) => new AgentTurnResult(
35-
PendingApprovalSummary,
36-
SessionPhase.Paused,
37-
ApprovalState.Pending,
38-
[CreateArtifact(request.SessionId, ExecuteArtifact, ArtifactKind.Snapshot)]),
39-
AgentExecutionMode.Execute => new AgentTurnResult(
40-
ExecuteSummary,
41-
SessionPhase.Execute,
42-
ApprovalState.NotRequired,
43-
[CreateArtifact(request.SessionId, ExecuteArtifact, ArtifactKind.Snapshot)]),
44-
AgentExecutionMode.Review => new AgentTurnResult(
45-
ReviewSummary,
46-
SessionPhase.Review,
47-
ApprovalState.Approved,
48-
[CreateArtifact(request.SessionId, ReviewArtifact, ArtifactKind.Report)]),
49-
_ => throw new UnreachableException(),
43+
AgentExecutionMode.Plan => Result<AgentTurnResult>.Succeed(
44+
new AgentTurnResult(
45+
PlanSummary,
46+
SessionPhase.Plan,
47+
ApprovalState.NotRequired,
48+
[CreateArtifact(request.SessionId, PlanArtifact, ArtifactKind.Plan)])),
49+
AgentExecutionMode.Execute when RequiresApproval(request.Prompt) => Result<AgentTurnResult>.Succeed(
50+
new AgentTurnResult(
51+
PendingApprovalSummary,
52+
SessionPhase.Paused,
53+
ApprovalState.Pending,
54+
[CreateArtifact(request.SessionId, ExecuteArtifact, ArtifactKind.Snapshot)])),
55+
AgentExecutionMode.Execute => Result<AgentTurnResult>.Succeed(
56+
new AgentTurnResult(
57+
ExecuteSummary,
58+
SessionPhase.Execute,
59+
ApprovalState.NotRequired,
60+
[CreateArtifact(request.SessionId, ExecuteArtifact, ArtifactKind.Snapshot)])),
61+
AgentExecutionMode.Review => Result<AgentTurnResult>.Succeed(
62+
new AgentTurnResult(
63+
ReviewSummary,
64+
SessionPhase.Review,
65+
ApprovalState.Approved,
66+
[CreateArtifact(request.SessionId, ReviewArtifact, ArtifactKind.Report)])),
67+
_ => Result<AgentTurnResult>.Fail(RuntimeCommunicationProblems.OrchestrationUnavailable()),
5068
});
5169
}
5270

DotPilot.Tests/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
global using DotPilot.Core.Features.ApplicationShell;
22
global using DotPilot.Core.Features.ControlPlaneDomain;
3+
global using DotPilot.Core.Features.RuntimeCommunication;
34
global using DotPilot.Core.Features.RuntimeFoundation;
45
global using DotPilot.Runtime.Features.RuntimeFoundation;
56
global using FluentAssertions;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
namespace DotPilot.Tests;
2+
3+
public class RuntimeCommunicationProblemsTests
4+
{
5+
[TestCase(ProviderConnectionStatus.Unavailable, RuntimeCommunicationProblemCode.ProviderUnavailable, System.Net.HttpStatusCode.ServiceUnavailable)]
6+
[TestCase(ProviderConnectionStatus.RequiresAuthentication, RuntimeCommunicationProblemCode.ProviderAuthenticationRequired, System.Net.HttpStatusCode.Unauthorized)]
7+
[TestCase(ProviderConnectionStatus.Misconfigured, RuntimeCommunicationProblemCode.ProviderMisconfigured, System.Net.HttpStatusCode.FailedDependency)]
8+
[TestCase(ProviderConnectionStatus.Outdated, RuntimeCommunicationProblemCode.ProviderOutdated, System.Net.HttpStatusCode.PreconditionFailed)]
9+
public void ProviderUnavailableMapsStatusesToExplicitProblemCodes(
10+
ProviderConnectionStatus status,
11+
RuntimeCommunicationProblemCode expectedCode,
12+
System.Net.HttpStatusCode expectedStatusCode)
13+
{
14+
var problem = RuntimeCommunicationProblems.ProviderUnavailable(status, "Codex");
15+
16+
problem.HasErrorCode(expectedCode).Should().BeTrue();
17+
problem.StatusCode.Should().Be((int)expectedStatusCode);
18+
problem.Detail.Should().Contain("Codex");
19+
}
20+
21+
[Test]
22+
public void ProviderUnavailableRejectsAvailableStatus()
23+
{
24+
var action = () => RuntimeCommunicationProblems.ProviderUnavailable(ProviderConnectionStatus.Available, "Codex");
25+
26+
action.Should().Throw<ArgumentOutOfRangeException>();
27+
}
28+
29+
[Test]
30+
public void ProviderUnavailableRejectsBlankProviderNames()
31+
{
32+
var action = () => RuntimeCommunicationProblems.ProviderUnavailable(ProviderConnectionStatus.Unavailable, " ");
33+
34+
action.Should().Throw<ArgumentException>();
35+
}
36+
37+
[Test]
38+
public void InvalidPromptCreatesValidationProblem()
39+
{
40+
var problem = RuntimeCommunicationProblems.InvalidPrompt();
41+
42+
problem.HasErrorCode(RuntimeCommunicationProblemCode.PromptRequired).Should().BeTrue();
43+
problem.StatusCode.Should().Be((int)System.Net.HttpStatusCode.BadRequest);
44+
problem.InvalidField("Prompt").Should().BeTrue();
45+
}
46+
47+
[Test]
48+
public void RuntimeHostUnavailableCreatesServiceUnavailableProblem()
49+
{
50+
var problem = RuntimeCommunicationProblems.RuntimeHostUnavailable();
51+
52+
problem.HasErrorCode(RuntimeCommunicationProblemCode.RuntimeHostUnavailable).Should().BeTrue();
53+
problem.StatusCode.Should().Be((int)System.Net.HttpStatusCode.ServiceUnavailable);
54+
}
55+
56+
[Test]
57+
public void OrchestrationUnavailableCreatesServiceUnavailableProblem()
58+
{
59+
var problem = RuntimeCommunicationProblems.OrchestrationUnavailable();
60+
61+
problem.HasErrorCode(RuntimeCommunicationProblemCode.OrchestrationUnavailable).Should().BeTrue();
62+
problem.StatusCode.Should().Be((int)System.Net.HttpStatusCode.ServiceUnavailable);
63+
}
64+
65+
[Test]
66+
public void PolicyRejectedCreatesForbiddenProblem()
67+
{
68+
var problem = RuntimeCommunicationProblems.PolicyRejected("file-write policy");
69+
70+
problem.HasErrorCode(RuntimeCommunicationProblemCode.PolicyRejected).Should().BeTrue();
71+
problem.StatusCode.Should().Be((int)System.Net.HttpStatusCode.Forbidden);
72+
problem.Detail.Should().Contain("file-write policy");
73+
}
74+
75+
[Test]
76+
public void PolicyRejectedRejectsBlankPolicyNames()
77+
{
78+
var action = () => RuntimeCommunicationProblems.PolicyRejected(" ");
79+
80+
action.Should().Throw<ArgumentException>();
81+
}
82+
}

0 commit comments

Comments
 (0)