Platform referenceReference
Agent HITL
Human-in-the-loop policy registry and evaluator for agent actions.
The @fabricorg/platform/agent-hitl module decides how an agent's proposed action is routed: auto-execute, needs-approval, escalate, or rejected. The platform supplies the registry and types; each vertical supplies the actual policy evaluator.
Install
pnpm add @fabricorg/platformdotnet add package FabricOrg.Platform.Abstractions
dotnet add package FabricOrg.PlatformTypes
export type AgentActionRoute = "auto-execute" | "needs-approval" | "escalate" | "rejected";
export type AgentActionRiskTier = "low" | "medium" | "high";
export interface AgentActionPolicyContext {
actionId: string;
actorId: string;
actorType: string;
tenantId: string;
spaceId: string;
parameters: Record<string, unknown>;
confidence?: number;
riskTier?: AgentActionRiskTier;
agentSessionId?: string;
agentRunId?: string;
}
export interface AgentActionPolicyDecision {
route: AgentActionRoute;
riskTier: AgentActionRiskTier;
reason: string;
}
export type AgentActionPolicyEvaluator = (
context: AgentActionPolicyContext,
) => AgentActionPolicyDecision | Promise<AgentActionPolicyDecision>;using FabricOrg.Platform.Actions;
using FabricOrg.Platform.Policies;
// AgentActionRoute is an abstract discriminated union
public abstract class AgentActionRoute
{
public sealed class AutoExecute : AgentActionRoute { }
public sealed class NeedsApproval : AgentActionRoute { }
public sealed class Escalated : AgentActionRoute { }
public sealed class Rejected : AgentActionRoute { }
}
public enum AgentActionRiskTier { Low, Medium, High }
public sealed class AgentActionPolicyContext
{
public ActionId ActionId { get; }
public string ActorId { get; }
public ActorType ActorType { get; }
public string TenantId { get; }
public string SpaceId { get; }
public IReadOnlyDictionary<string, object?> Parameters { get; }
public double? Confidence { get; }
public AgentActionRiskTier? RiskTier { get; }
public string? AgentSessionId { get; }
public string? AgentRunId { get; }
}
public sealed class AgentActionPolicyDecision
{
public AgentActionRoute Route { get; }
public AgentActionRiskTier RiskTier { get; }
public string Reason { get; }
}
public delegate Task<AgentActionPolicyDecision> AgentActionPolicyEvaluator(
AgentActionPolicyContext context, CancellationToken ct);Registry
Register one evaluator per action ID. The registry throws if you try to register a different evaluator for the same action (fail-closed). Re-registering the same evaluator is idempotent.
import { AgentActionPolicyRegistry } from "@fabricorg/platform/agent-hitl";
const registry = new AgentActionPolicyRegistry();
registry.register({
actionId: "orders.send_quote",
evaluator: (ctx) => {
const { amount } = ctx.parameters as { amount: number };
if (amount > 50_000) {
return {
route: "needs-approval",
riskTier: "high",
reason: "Amount exceeds $50k threshold",
};
}
return { route: "auto-execute", riskTier: "low", reason: "Under threshold" };
},
});
const def = registry.resolve("orders.send_quote");
const decision = await def!.evaluator({
actionId: "orders.send_quote",
actorId: "agent-1",
actorType: "agent",
tenantId: "tenant-1",
spaceId: "space-1",
parameters: { amount: 75_000 },
});
// decision.route === "needs-approval"using FabricOrg.Platform.Policies;
using FabricOrg.Platform.Registries;
var registry = new AgentActionPolicyRegistry();
registry.Register(new AgentActionPolicyDefinition(
new ActionId("orders", "send_quote"),
async (ctx, ct) =>
{
var amount = (double)(ctx.Parameters["amount"] ?? 0);
if (amount > 50_000)
{
return new AgentActionPolicyDecision(
new AgentActionRoute.NeedsApproval(),
AgentActionRiskTier.High,
"Amount exceeds $50k threshold");
}
return new AgentActionPolicyDecision(
new AgentActionRoute.AutoExecute(),
AgentActionRiskTier.Low,
"Under threshold");
}));
var def = registry.Resolve(new ActionId("orders", "send_quote"));
var decision = await def!.Evaluator(new AgentActionPolicyContext(
new ActionId("orders", "send_quote"),
"agent-1",
ActorType.Agent,
"tenant-1",
"space-1",
new Dictionary<string, object?> { ["amount"] = 75_000 }), CancellationToken.None);
// decision.Route is AgentActionRoute.NeedsApprovalSingleton bridge pattern
Most applications don't use the registry directly. Instead, the platform layer exposes a singleton pointer that verticals wire up at boot:
// platform layer — vertical-agnostic
import { type AgentActionPolicyEvaluator } from "@fabricorg/platform/agent-hitl";
let registered: AgentActionPolicyEvaluator | null = null;
export function registerAgentActionPolicy(evaluator: AgentActionPolicyEvaluator): void {
registered = evaluator;
}
export async function evaluateRegisteredAgentActionPolicy(
input: Partial<AgentActionPolicyContext> & Pick<AgentActionPolicyContext, "actionId">,
): Promise<AgentActionPolicyDecision> {
if (registered) {
return await registered({
actorId: "system",
actorType: "system",
tenantId: "system",
spaceId: "system",
parameters: {},
...input,
});
}
return {
route: "auto-execute",
riskTier: "low",
reason: "No agent action policy registered; defaulting to auto-execute.",
};
}// platform layer — vertical-agnostic
using FabricOrg.Platform.Policies;
public static class AgentActionPolicyBridge
{
private static AgentActionPolicyEvaluator? _registered;
public static void Register(AgentActionPolicyEvaluator evaluator)
=> _registered = evaluator;
public static async Task<AgentActionPolicyDecision> EvaluateAsync(
AgentActionPolicyContext context,
CancellationToken ct = default)
{
if (_registered is not null)
return await _registered(context, ct);
return new AgentActionPolicyDecision(
new AgentActionRoute.AutoExecute(),
AgentActionRiskTier.Low,
"No agent action policy registered; defaulting to auto-execute.");
}
}Vertical evaluator example
A real lending evaluator might look like this:
export function evaluateAgentActionPolicy(
context: AgentActionPolicyContext,
): AgentActionPolicyDecision {
const { actionId, confidence } = context;
// Blacklist — underwriting actions are never delegable
if (FORBIDDEN_ACTION_IDS.has(actionId)) {
return { route: "rejected", riskTier: "high", reason: "Never delegable to agents." };
}
switch (actionId) {
case "lending.update_data_from_borrower_response": {
if (confidence == null) return { route: "needs-approval", riskTier: "medium", reason: "Missing confidence" };
if (confidence >= 0.85) return { route: "auto-execute", riskTier: "low", reason: `confidence=${confidence}` };
if (confidence >= 0.6) return { route: "needs-approval", riskTier: "medium", reason: "Low confidence" };
return { route: "escalate", riskTier: "high", reason: "Unreliable extraction" };
}
case "lending.satisfy_stipulation":
return { route: "needs-approval", riskTier: "high", reason: "Financial impact" };
default:
return { route: "rejected", riskTier: "high", reason: "Not in whitelist" };
}
}public static AgentActionPolicyDecision EvaluateAgentActionPolicy(AgentActionPolicyContext ctx)
{
var actionId = ctx.ActionId.ToString();
var confidence = ctx.Confidence;
// Blacklist — underwriting actions are never delegable
if (ForbiddenActionIds.Contains(actionId))
{
return new AgentActionPolicyDecision(
new AgentActionRoute.Rejected(),
AgentActionRiskTier.High,
"Never delegable to agents.");
}
switch (actionId)
{
case "lending.update_data_from_borrower_response":
if (confidence is null)
return new AgentActionPolicyDecision(
new AgentActionRoute.NeedsApproval(),
AgentActionRiskTier.Medium,
"Missing confidence");
if (confidence >= 0.85)
return new AgentActionPolicyDecision(
new AgentActionRoute.AutoExecute(),
AgentActionRiskTier.Low,
$"confidence={confidence}");
if (confidence >= 0.6)
return new AgentActionPolicyDecision(
new AgentActionRoute.NeedsApproval(),
AgentActionRiskTier.Medium,
"Low confidence");
return new AgentActionPolicyDecision(
new AgentActionRoute.Escalated(),
AgentActionRiskTier.High,
"Unreliable extraction");
case "lending.satisfy_stipulation":
return new AgentActionPolicyDecision(
new AgentActionRoute.NeedsApproval(),
AgentActionRiskTier.High,
"Financial impact");
default:
return new AgentActionPolicyDecision(
new AgentActionRoute.Rejected(),
AgentActionRiskTier.High,
"Not in whitelist");
}
}Best practices
- Whitelist model. Any action not explicitly listed should return
rejected. This prevents new actions from silently becoming auto-executable. - Confidence-gate inferred data. Actions that consume LLM-extracted data should gate on a confidence threshold.
- Never auto-allow financial mutations.
satisfy_stipulation,create_offer,record_funding— alwaysneeds-approvalorrejected. - Keep the evaluator pure. No DB calls, no side effects. The evaluator receives context and returns a decision.
- Register at boot. Call
registerAgentActionPolicy(evaluator)in your module initialization so the platform endpoint can dispatch through it.