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 inai_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
Related
- ai.gateway PRD + Phase 2 architecture
AppCapabilities.AiGateway— capability string cataloghexasync.services.notification.INotificationServiceV2— sibling inner-service with the same proxy pattern; use as a template when adding a second method to this contract
No packages depend on hexasync.ai.gateway.contracts.
.NET 10.0
- hexasync.domain.sso.models (>= 1.10.1)
- hexasync.services.proxy (>= 1.10.1)