Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.pecta.ai/llms.txt

Use this file to discover all available pages before exploring further.

Every built-in gate in Pecta is an object that satisfies the Gate interface. You can implement the same interface yourself to add checks that are specific to your domain — for example, enforcing a proprietary response schema, calling an internal moderation API, or checking that a bid came from an allowlisted seat. Custom gates compose with built-in gates in createEngine the same way built-in gates do. The engine handles parallelism, the timeout budget, fail-fast signalling, and result assembly automatically.

The Gate interface

export interface Gate {
  name: string;
  run: GateRun;
}
name is the string recorded in result.gates and used for duplicate detection. It must be unique across all gates in an engine.

The GateRun signature

export type GateRun = (
  ctx: EvaluationContext,
  signal: AbortSignal
) => Promise<GateOutcome> | GateOutcome;
run receives the full evaluation context and an AbortSignal from the engine’s shared AbortController. It can return a GateOutcome synchronously or as a Promise.

The GateOutcome type

export type GateOutcome = {
  passed: boolean;
  reason?: string;
  skipped?: boolean;
  details?: unknown;
}
FieldDescription
passedtrue if the check passed, false if it failed.
reasonHuman-readable explanation of why the gate failed or was skipped. Optional on success.
skippedSet to true to opt out of evaluation for this call. Skipped gates are treated as passing.
detailsAny structured data useful for debugging — Zod issues, matched tokens, API response fragments. Never include user payloads.
The engine fills in name and latency_ms automatically. You do not need to set them in your run function.

A complete custom gate

This gate verifies that the output’s bid_id field appears in an in-memory allowlist. It checks signal.aborted before doing any work, which is the correct pattern for gates that might do expensive I/O:
import type { Gate, EvaluationContext, GateOutcome } from "@pecta/core";

const ALLOWED_SEATS = new Set(["seat-001", "seat-002", "seat-099"]);

const seatAllowlistGate: Gate = {
  name: "seat.allowlist",
  run(ctx: EvaluationContext, signal: AbortSignal): GateOutcome {
    // Always check signal.aborted before expensive work.
    if (signal.aborted) {
      return { passed: false, reason: "aborted" };
    }

    const output = ctx.output as Record<string, unknown>;
    const seat = typeof output?.seat === "string" ? output.seat : undefined;

    if (seat === undefined) {
      return { passed: false, reason: "missing seat field in output" };
    }

    if (!ALLOWED_SEATS.has(seat)) {
      return {
        passed: false,
        reason: `seat "${seat}" is not in the allowlist`,
        details: { seat, allowed: [...ALLOWED_SEATS] },
      };
    }

    return { passed: true };
  },
};

Using an async gate

When your gate needs to call an external service, return a Promise. The engine awaits it and applies the timeout budget the same way as synchronous gates:
import type { Gate } from "@pecta/core";

const moderationGate: Gate = {
  name: "moderation.api",
  async run(ctx, signal) {
    if (signal.aborted) return { passed: false, reason: "aborted" };

    const response = await fetch("https://internal.example.com/moderate", {
      method: "POST",
      signal, // forward the AbortSignal so the fetch is cancelled on timeout
      body: JSON.stringify({ text: ctx.output }),
      headers: { "Content-Type": "application/json" },
    });

    const { safe } = await response.json() as { safe: boolean };
    return safe
      ? { passed: true }
      : { passed: false, reason: "moderation API flagged output" };
  },
};
Forward the signal to any fetch or other cancellable API call inside your gate. If you do not, a network request will keep running even after the engine’s timeout fires, and the Promise will not resolve until the network responds — causing the engine to wait beyond its timeout budget.

Passing your gate to createEngine

Custom gates go in the same gates array as built-in gates:
import { createEngine, gates } from "@pecta/core";

const engine = createEngine({
  gates: [
    gates.content(),
    gates.pii(),
    seatAllowlistGate,       // custom gate
    moderationGate,          // another custom gate
  ],
  timeout: 50,
});

const result = await engine.evaluate({
  agent_id: "dsp-bidder",
  output: { seat: "seat-999", text: "Buy now!" },
});

What the engine fills in automatically

You only return GateOutcome from run. The engine wraps it into a full GateResult before adding it to result.gates:
// What you return from run:
{ passed: false, reason: 'seat "seat-999" is not in the allowlist' }

// What appears in result.gates:
{
  name: "seat.allowlist",          // from gate.name
  passed: false,
  reason: 'seat "seat-999" is not in the allowlist',
  latency_ms: 0.23                 // measured by the engine
}

Skipping conditionally

Return { passed: true, skipped: true } when your gate cannot or should not evaluate this particular context. Skipped results appear in result.gates but do not cause result.passed to be false:
run(ctx, signal): GateOutcome {
  if (ctx.tool !== "bidder.respond") {
    return { passed: true, skipped: true, reason: "not a bid response" };
  }
  // ...rest of logic
}
The GateRun type is exported from @pecta/core alongside Gate, GateOutcome, and EvaluationContext. Import all the types you need directly from the package — you do not need to import from sub-paths.