Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,11 @@ For this app:
- `DotPilot/DotPilot.csproj` keeps `GenerateDocumentationFile=true` with `CS1591` suppressed so `IDE0005` stays enforceable in CI across all target frameworks without inventing command-line-only build flags
- architecture work must keep a vertical-slice shape: each feature owns its contracts, orchestration, and tests behind clear boundaries instead of growing a shared horizontal service layer
- keep the Uno app project presentation-only; domain, runtime host, orchestration, integrations, and persistence code must live in separate class-library projects so UI composition does not mix with feature implementation
- when the user asks to implement an epic, the delivery branch and PR must cover all of that epic's direct child issues that belong to the requested scope, not just one child issue with a partial close-out
- epic implementation PRs must include automated tests for every direct child issue they claim to cover, plus the broader runtime and UI regressions required by the touched flows
- do not claim an epic is implemented unless every direct child issue in the requested scope is both realized in code and covered by automated tests; partial coverage is not an acceptable close-out
- structure both `DotPilot.Tests` and `DotPilot.UITests` by vertical slice and explicit harness boundaries; do not keep test files in one flat project-root pile
- the first embedded Orleans host cut must use `UseLocalhostClustering` plus in-memory grain storage and reminders; do not introduce remote clustering or external durable stores until a later backlog item explicitly requires them
- the first embedded Orleans runtime cut must use `UseLocalhostClustering` together with in-memory Orleans grain storage and in-memory reminders; do not introduce remote clustering or external durable stores until a later backlog item explicitly requires them, and keep durable resume/replay outside Orleans storage until the cluster topology is intentionally upgraded
- GitHub is the backlog, not the product: use issues and PRs only to drive task scope and traceability, and never copy GitHub issue text, labels, workflow language, or tracker metadata into production code, runtime snapshots, or user-facing UI
- never claim an epic is complete until its current GitHub scope is verified against the live issue graph; check which issues are real children versus issues that merely depend on the epic or belong to a different parent epic
- Desktop responsiveness is a product requirement: avoid synchronous probe, filesystem, network, or process work on UI-facing construction and navigation paths so the app stays fast and immediately reactive
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-rc4" />
<PackageVersion Include="Microsoft.Orleans.Core.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Orleans.Persistence.Memory" Version="10.0.1" />
<PackageVersion Include="Microsoft.Orleans.Reminders" Version="10.0.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ public enum RuntimeCommunicationProblemCode
RuntimeHostUnavailable,
OrchestrationUnavailable,
PolicyRejected,
SessionArchiveMissing,
ResumeCheckpointMissing,
SessionArchiveCorrupted,
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public static class RuntimeCommunicationProblems
private const string RuntimeHostUnavailableDetail = "The embedded runtime host is unavailable for the requested operation.";
private const string OrchestrationUnavailableDetail = "The orchestration runtime is unavailable for the requested operation.";
private const string PolicyRejectedFormat = "The requested action was rejected by policy: {0}.";
private const string SessionArchiveMissingFormat = "No persisted runtime session archive exists for session {0}.";
private const string ResumeCheckpointMissingFormat = "Session {0} does not have a checkpoint that can be resumed.";
private const string SessionArchiveCorruptedFormat = "Session {0} has corrupted persisted runtime state.";

public static Problem InvalidPrompt()
{
Expand Down Expand Up @@ -86,6 +89,33 @@ public static Problem PolicyRejected(string policyName)
HttpStatusCode.Forbidden);
}

public static Problem SessionArchiveMissing(SessionId sessionId)
{
return CreateProblem(
RuntimeCommunicationProblemCode.SessionArchiveMissing,
SessionArchiveMissingFormat,
sessionId.ToString(),
HttpStatusCode.NotFound);
}

public static Problem ResumeCheckpointMissing(SessionId sessionId)
{
return CreateProblem(
RuntimeCommunicationProblemCode.ResumeCheckpointMissing,
ResumeCheckpointMissingFormat,
sessionId.ToString(),
HttpStatusCode.Conflict);
}

public static Problem SessionArchiveCorrupted(SessionId sessionId)
{
return CreateProblem(
RuntimeCommunicationProblemCode.SessionArchiveCorrupted,
SessionArchiveCorruptedFormat,
sessionId.ToString(),
HttpStatusCode.InternalServerError);
}

private static Problem CreateProblem(
RuntimeCommunicationProblemCode code,
string detailFormat,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace DotPilot.Core.Features.RuntimeFoundation;

public sealed record EmbeddedRuntimeTrafficTransitionDescriptor(
string Source,
string Target,
IReadOnlyList<string> SourceMethods,
IReadOnlyList<string> TargetMethods,
bool IsReentrant);

public sealed record EmbeddedRuntimeTrafficPolicySnapshot(
int IssueNumber,
string IssueLabel,
string Summary,
string MermaidDiagram,
IReadOnlyList<EmbeddedRuntimeTrafficTransitionDescriptor> AllowedTransitions);

public sealed record EmbeddedRuntimeTrafficProbe(
Type SourceGrainType,
string SourceMethod,
Type TargetGrainType,
string TargetMethod);

public sealed record EmbeddedRuntimeTrafficDecision(
bool IsAllowed,
string MermaidDiagram);

public interface IEmbeddedRuntimeTrafficPolicyCatalog
{
EmbeddedRuntimeTrafficPolicySnapshot GetSnapshot();

EmbeddedRuntimeTrafficDecision Evaluate(EmbeddedRuntimeTrafficProbe probe);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using DotPilot.Core.Features.ControlPlaneDomain;
using ManagedCode.Communication;

namespace DotPilot.Core.Features.RuntimeFoundation;

public interface IAgentRuntimeClient
{
ValueTask<Result<AgentTurnResult>> ExecuteAsync(AgentTurnRequest request, CancellationToken cancellationToken);

ValueTask<Result<AgentTurnResult>> ResumeAsync(AgentTurnResumeRequest request, CancellationToken cancellationToken);

ValueTask<Result<RuntimeSessionArchive>> GetSessionArchiveAsync(SessionId sessionId, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public static class RuntimeFoundationIssues
public const int CommunicationContracts = 23;
public const int EmbeddedOrleansHost = 24;
public const int AgentFrameworkRuntime = 25;
public const int GrainTrafficPolicy = 26;
public const int SessionPersistence = 27;

public static string FormatIssueLabel(int issueNumber) => string.Concat(IssuePrefix, issueNumber);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using DotPilot.Core.Features.ControlPlaneDomain;

namespace DotPilot.Core.Features.RuntimeFoundation;

public sealed record AgentTurnResumeRequest(
SessionId SessionId,
ApprovalState ApprovalState,
string Summary);

public sealed record RuntimeSessionReplayEntry(
string Kind,
string Summary,
SessionPhase Phase,
ApprovalState ApprovalState,
DateTimeOffset RecordedAt);

public sealed record RuntimeSessionArchive(
SessionId SessionId,
string WorkflowSessionId,
SessionPhase Phase,
ApprovalState ApprovalState,
DateTimeOffset UpdatedAt,
string? CheckpointId,
IReadOnlyList<RuntimeSessionReplayEntry> Replay,
IReadOnlyList<ArtifactDescriptor> Artifacts);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static IHostBuilder UseDotPilotEmbeddedRuntime(
services.AddSingleton(resolvedOptions);
services.AddSingleton<EmbeddedRuntimeHostCatalog>();
services.AddSingleton<IEmbeddedRuntimeHostCatalog>(serviceProvider => serviceProvider.GetRequiredService<EmbeddedRuntimeHostCatalog>());
services.AddSingleton<IEmbeddedRuntimeTrafficPolicyCatalog, EmbeddedRuntimeTrafficPolicyCatalog>();
services.AddHostedService<EmbeddedRuntimeHostLifecycleService>();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ internal static class EmbeddedRuntimeHostNames
public const int DefaultSiloPort = 11_111;
public const int DefaultGatewayPort = 30_000;
public const string GrainStorageProviderName = "runtime-foundation-memory";
public const string ClientSourceName = "Client";
public const string ClientSourceMethodName = "Invoke";
public const string SessionStateName = "session";
public const string WorkspaceStateName = "workspace";
public const string FleetStateName = "fleet";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using DotPilot.Core.Features.RuntimeFoundation;

namespace DotPilot.Runtime.Host.Features.RuntimeFoundation;

internal static class EmbeddedRuntimeTrafficPolicy
{
private const string PolicySummary =
"Client and grain transitions stay explicit so the embedded host can reject unsupported hops before the runtime model grows.";
private const string MermaidHeader = "flowchart LR";
private const string MermaidArrow = " --> ";
private const string MermaidActiveArrow = " ==> ";

public static string Summary => PolicySummary;

public static IReadOnlyList<EmbeddedRuntimeTrafficTransitionDescriptor> AllowedTransitions =>
[
CreateClientTransition(EmbeddedRuntimeHostNames.SessionGrainName),
CreateClientTransition(EmbeddedRuntimeHostNames.WorkspaceGrainName),
CreateClientTransition(EmbeddedRuntimeHostNames.FleetGrainName),
CreateClientTransition(EmbeddedRuntimeHostNames.PolicyGrainName),
CreateClientTransition(EmbeddedRuntimeHostNames.ArtifactGrainName),
CreateTransition(EmbeddedRuntimeHostNames.SessionGrainName, EmbeddedRuntimeHostNames.WorkspaceGrainName, nameof(ISessionGrain.UpsertAsync), nameof(IWorkspaceGrain.GetAsync)),
CreateTransition(EmbeddedRuntimeHostNames.SessionGrainName, EmbeddedRuntimeHostNames.FleetGrainName, nameof(ISessionGrain.UpsertAsync), nameof(IFleetGrain.GetAsync)),
CreateTransition(EmbeddedRuntimeHostNames.SessionGrainName, EmbeddedRuntimeHostNames.PolicyGrainName, nameof(ISessionGrain.UpsertAsync), nameof(IPolicyGrain.GetAsync)),
CreateTransition(EmbeddedRuntimeHostNames.SessionGrainName, EmbeddedRuntimeHostNames.ArtifactGrainName, nameof(ISessionGrain.UpsertAsync), nameof(IArtifactGrain.UpsertAsync)),
CreateTransition(EmbeddedRuntimeHostNames.FleetGrainName, EmbeddedRuntimeHostNames.PolicyGrainName, nameof(IFleetGrain.GetAsync), nameof(IPolicyGrain.GetAsync)),
];

public static bool IsAllowed(EmbeddedRuntimeTrafficProbe probe)
{
ArgumentNullException.ThrowIfNull(probe);

var sourceName = GetGrainName(probe.SourceGrainType);
var targetName = GetGrainName(probe.TargetGrainType);

return AllowedTransitions.Any(transition =>
string.Equals(transition.Source, sourceName, StringComparison.Ordinal) &&
string.Equals(transition.Target, targetName, StringComparison.Ordinal) &&
transition.SourceMethods.Contains(probe.SourceMethod, StringComparer.Ordinal) &&
transition.TargetMethods.Contains(probe.TargetMethod, StringComparer.Ordinal));
}

public static string CreateMermaidDiagram()
{
return CreateMermaidDiagramCore(activeTransition: null);
}

public static string CreateMermaidDiagram(EmbeddedRuntimeTrafficProbe probe)
{
ArgumentNullException.ThrowIfNull(probe);

var activeTransition = (
Source: GetGrainName(probe.SourceGrainType),
Target: GetGrainName(probe.TargetGrainType),
SourceMethod: probe.SourceMethod,
TargetMethod: probe.TargetMethod);
return CreateMermaidDiagramCore(activeTransition);
}

private static EmbeddedRuntimeTrafficTransitionDescriptor CreateClientTransition(string target)
{
return new(
EmbeddedRuntimeHostNames.ClientSourceName,
target,
[EmbeddedRuntimeHostNames.ClientSourceMethodName],
[nameof(ISessionGrain.GetAsync), nameof(ISessionGrain.UpsertAsync)],
false);
}

private static EmbeddedRuntimeTrafficTransitionDescriptor CreateTransition(
string source,
string target,
string sourceMethod,
string targetMethod)
{
return new(
source,
target,
[sourceMethod],
[targetMethod],
false);
}

private static string GetGrainName(Type grainType)
{
ArgumentNullException.ThrowIfNull(grainType);

return grainType == typeof(ISessionGrain) ? EmbeddedRuntimeHostNames.SessionGrainName
: grainType == typeof(IWorkspaceGrain) ? EmbeddedRuntimeHostNames.WorkspaceGrainName
: grainType == typeof(IFleetGrain) ? EmbeddedRuntimeHostNames.FleetGrainName
: grainType == typeof(IPolicyGrain) ? EmbeddedRuntimeHostNames.PolicyGrainName
: grainType == typeof(IArtifactGrain) ? EmbeddedRuntimeHostNames.ArtifactGrainName
: grainType.Name;
}

private static string CreateMermaidDiagramCore((string Source, string Target, string SourceMethod, string TargetMethod)? activeTransition)
{
var lines = new List<string>(AllowedTransitions.Count + 1)
{
MermaidHeader,
};

foreach (var transition in AllowedTransitions)
{
var isActive = activeTransition is not null &&
string.Equals(transition.Source, activeTransition.Value.Source, StringComparison.Ordinal) &&
string.Equals(transition.Target, activeTransition.Value.Target, StringComparison.Ordinal) &&
transition.SourceMethods.Contains(activeTransition.Value.SourceMethod, StringComparer.Ordinal) &&
transition.TargetMethods.Contains(activeTransition.Value.TargetMethod, StringComparer.Ordinal);
var arrow = isActive ? MermaidActiveArrow : MermaidArrow;
lines.Add(
string.Concat(
transition.Source,
arrow,
transition.Target,
" : ",
string.Join(", ", transition.SourceMethods),
" -> ",
string.Join(", ", transition.TargetMethods)));
}

return string.Join(Environment.NewLine, lines);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using DotPilot.Core.Features.RuntimeFoundation;

namespace DotPilot.Runtime.Host.Features.RuntimeFoundation;

internal sealed class EmbeddedRuntimeTrafficPolicyCatalog : IEmbeddedRuntimeTrafficPolicyCatalog
{
public EmbeddedRuntimeTrafficPolicySnapshot GetSnapshot()
{
return new(
RuntimeFoundationIssues.GrainTrafficPolicy,
RuntimeFoundationIssues.FormatIssueLabel(RuntimeFoundationIssues.GrainTrafficPolicy),
EmbeddedRuntimeTrafficPolicy.Summary,
EmbeddedRuntimeTrafficPolicy.CreateMermaidDiagram(),
EmbeddedRuntimeTrafficPolicy.AllowedTransitions);
}

public EmbeddedRuntimeTrafficDecision Evaluate(EmbeddedRuntimeTrafficProbe probe)
{
ArgumentNullException.ThrowIfNull(probe);

return new EmbeddedRuntimeTrafficDecision(
EmbeddedRuntimeTrafficPolicy.IsAllowed(probe),
EmbeddedRuntimeTrafficPolicy.CreateMermaidDiagram(probe));
}
}
5 changes: 5 additions & 0 deletions DotPilot.Runtime/DotPilot.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DotPilot.Core\DotPilot.Core.csproj" />
</ItemGroup>
Expand Down
Loading