hexasync.ai.gateway.contracts 0.2.0

hexasync.ai.gateway.contracts

Public RPC contract and DTOs for the HexaSync AI Gateway. Install this package when another backend service needs to request AI-generated insights from the central gateway — all provider API keys, prompt versioning, and MCP orchestration stay server-side, callers never touch AI credentials.

Stability. The shapes in this package are frozen. Renaming a field or a method is a coordinated cross-repo release — see Versioning below.


What ships in this package

Type Kind Purpose
IAiGatewayService interface (IMicroService) RPC surface. Resolved through the ECDSA inner-service proxy.
FlowExecutionRequest record Input for the generic ExecuteFlowAsync.
FlowExecutionResult record Output of ExecuteFlowAsync — carries a JsonElement + metadata.
FlowExecutionMetadata record Provider / token / latency details on FlowExecutionResult.
VisitorBehaviorRequest record Input for the legacy AnalyzeVisitorBehaviorAsync (obsolete, removed in v1.0).
AiInsightAlert record Output of the legacy typed method. Also what the visitor-behavior/v1 flow returns via ExecuteFlowAsync once you Output.Deserialize<AiInsightAlert>().
VisitorLocation record Nested geography on AiInsightAlert.

Nothing else is public. Admin endpoints (/admin/providers/*, /admin/flows/*, test/simulate surface) are not part of this package — they're reached only by the control-center BFF over HTTP with capability gates, and aren't callable inner-service.


Install

<ItemGroup>
  <PackageReference Include="hexasync.ai.gateway.contracts" Version="0.2.*" />
</ItemGroup>

The package transitively pulls hexasync.services.proxy (for IMicroService + the factory) and hexasync.domain.sso.models (for UserViewModel via IMicroService.GetOwner). You don't reference those directly.


Register in your consumer

Same pattern as INotificationServiceV2 — no extra wiring on the contracts side, the inner-service factory resolves IAiGatewayService from your existing proxy registration.

// Program.cs or Startup.cs of the consumer service
builder.Services.AddMicroServiceProxy(builder.Configuration);         // already there
builder.Services.AddEcdsaInnerServiceResolver(builder.Configuration); // already there
// No ai-gateway-specific line — discovery is env-driven (see below).

Required environment variables on the consumer

Name Example Purpose
AI_GATEWAY_SERVICE_URL http://ai-gateway-service.hexasync-infras.svc.cluster.local:8080 Where the gateway listens. Read by the proxy factory (ServiceConstants.AI_GATEWAY_SERVICE_URL) to resolve IAiGatewayService.
SSO_SERVICE_URL http://sso-service.hexasync-infras.svc.cluster.local:8080 Already set; the ECDSA proxy uses it to mint inner-service tokens.
SSO_SERVICE_SECRET secret Already set; HMAC fallback for inner-service auth.

Required capability on the consumer's service identity

The gateway's insight endpoint is gated on ai_gateway:insights:generate. Grant it to the consumer's service app in sso.api — e.g. via the role attached to the client_credentials app identity. The cap string is exported as AppCapabilities.AiGateway.Insights.Generate in hexasync.auth.middleware.

Without this capability, ExecuteFlowAsync (and the legacy AnalyzeVisitorBehaviorAsync) will fail auth at the gateway and return null (soft-fail, see below).


Usage — generic ExecuteFlowAsync (preferred)

One method for every flow the admin UI registers. Adding a flow never requires a contract bump — consumers just start passing the new flow id.

using System.Text.Json;
using hexasync.ai.gateway.contracts;
using hexasync.services.proxy.ServiceFactory;

public sealed class VisitorInsightDispatcher(
    IMicroServiceFactory microServiceFactory,
    ILogger<VisitorInsightDispatcher> logger)
{
    public async Task HandleAsync(Guid userId, string ga4VisitorId, CancellationToken ct)
    {
        var aiGateway = microServiceFactory.GetECDSAInnerService<IAiGatewayService>();

        var result = await aiGateway.ExecuteFlowAsync(
            new FlowExecutionRequest(
                FlowId: "visitor-behavior/v1",
                Bindings: new Dictionary<string, object?>
                {
                    ["user_id"] = userId.ToString(),
                    ["ga4_visitor_id"] = ga4VisitorId,
                    ["filter_value"] = ga4VisitorId.Length > 36 ? ga4VisitorId[..36] : ga4VisitorId,
                }),
            ct);

        if (result is null)
        {
            // Soft-fail — upstream provider timed out / 5xx'd / returned no
            // usable signal. Dispatch the degraded path; don't retry on the
            // same request.
            logger.LogInformation("ai_gateway.no_signal user_id={UserId}", userId);
            await SendGenericNotificationAsync(userId, ct);
            return;
        }

        // Consumer owns the output schema. For the visitor-behavior flow the
        // prompt is authored to return an AiInsightAlert-shaped object.
        var insight = result.Output.Deserialize<AiInsightAlert>();
        if (insight is null)
        {
            logger.LogWarning(
                "ai_gateway.deserialise_failed flow={Flow} prompt={Prompt}",
                result.FlowId, result.PromptVersion);
            await SendGenericNotificationAsync(userId, ct);
            return;
        }

        await SendInsightNotificationAsync(userId, insight, result.Metadata, ct);
    }
}

Request shape

new FlowExecutionRequest(
    FlowId: "visitor-behavior/v1",                  // must match ai_gateway.flows.flow_id
    Bindings: new Dictionary<string, object?>
    {
        ["user_id"] = user.Id.ToString(),           // keys + types must match flow's bindings_schema
        ["ga4_visitor_id"] = ga4VisitorId,
        ["filter_value"] = filterValue,
    },
    TenantId: null,                                  // enables per-tenant flow/provider overrides
    MaxTokensOverride: null);                        // per-call cost cap; null uses flow default

Response shape

public sealed record FlowExecutionResult(
    string FlowId,                          // echoed back
    string PromptVersion,                   // what version actually ran (for audit / cache keys)
    JsonElement Output,                     // flow-specific JSON; consumer deserialises
    FlowExecutionMetadata Metadata);

public sealed record FlowExecutionMetadata(
    string ProviderId,                      // "anthropic" | "openai" | "gemini" | "bedrock"
    string Model,                           // e.g. "claude-haiku-4-5"
    int? InputTokens,
    int? OutputTokens,
    long LatencyMs,
    string? StopReason);

Output semantics: if the model returns JSON (the common case for structured flows), Output is that parsed JSON object. If it returns plain prose, the gateway wraps it as { "text": "..." } so consumers always get a JSON payload — never a raw string to guard.


Usage — legacy AnalyzeVisitorBehaviorAsync (obsolete)

Still works for one release cycle. Emits an [Obsolete] compiler warning on use. Under the hood it now delegates to the same executor as ExecuteFlowAsync; the only difference is it applies the GA4 visitor-id truncation and returns AiInsightAlert? directly (tolerant parsing via the server-side InsightParser — handles markdown-fenced JSON and GA4's "(not set)" placeholders).

// Still compiles; just migrate when convenient.
var insight = await aiGateway.AnalyzeVisitorBehaviorAsync(
    new VisitorBehaviorRequest(userId, ga4VisitorId),
    ct);

Migration — replace call sites with the ExecuteFlowAsync snippet above. The shape of AiInsightAlert doesn't change; only the method name and the binding construction move to the caller.

The obsolete method is removed in 1.0.0.


Soft-fail contract

Neither method throws on upstream provider errors. A null return is the contract's way of saying "no usable insight", and it covers:

  • Unknown FlowId (not in ai_gateway.flows)
  • Prompt render failure (bindings don't match the template's variables)
  • Provider API timeout / 4xx / 5xx
  • Flow points at an MCP the gateway doesn't have credentials for
  • Flow's provider kind isn't registered at runtime
  • Insight capability check failed on the gateway side

Callers MUST handle null by dispatching a graceful degraded flow (usually: send a generic notification). Do not retry on the same request — the gateway has already logged the root cause.

Network-layer faults (connection refused, DNS failure, ECDSA token signing failure) bubble up as the underlying HttpRequestException from hexasync.services.proxy. Catch at the call site only if your flow must survive the gateway being completely offline.


Reference — who consumes this today

Service Entry point Purpose
sso.api Visitor-insight background job Fires on user registration / login to generate a welcome-back notification

See sso.api/hexasync.domain.Account/Commands/*Notification* for the resolution pattern (microServiceFactory.GetECDSAInnerService<IAiGatewayService>()) — the shape mirrors the INotificationServiceV2 call sites in the same files.


Versioning

Package version: 0.2.x. Phase 2 target is 1.0.0 once ExecuteFlowAsync has been exercised in production for one release cycle and AnalyzeVisitorBehaviorAsync is removed.

Change Version bump Coordination
Add a new method on IAiGatewayService minor (0.3.0) Gateway side must ship first; consumers upgrade at their own pace.
Add a new optional field to FlowExecutionRequest patch (0.2.1) Back-compat — existing consumers unaffected.
Add a field to FlowExecutionMetadata patch (0.2.1) Back-compat — consumers reading by name keep working.
Rename / remove any field or method major (1.0.0) Coordinated cross-repo release. Update all known consumers in the same PR chain.
Remove AnalyzeVisitorBehaviorAsync major (1.0.0) All consumers must have migrated to ExecuteFlowAsync first.

Breaking changes require updating every row in the "who consumes this today" table above. Check consumers with:

gh search code 'hexasync.ai.gateway.contracts' --owner hexasync-saas

No packages depend on hexasync.ai.gateway.contracts.

Version Downloads Last updated
0.2.0 2 04/25/2026
0.1.0 1 04/21/2026