Skip to main content

TypeScript Agent Workflow

This workflow uses the OpenAI client directly because @keeptrusts/agent 0.1.0 cannot currently be imported by an ordinary Node.js process. It sends a governed model request through the gateway and verifies the request against the control-plane event API.

Prerequisites

  • A server-side Node.js runtime with global fetch and the node:crypto APIs
  • An existing Keeptrusts agent ID, linked and deployed when the connected gateway mode requires an agent binding
  • A gateway URL ending in /v1
  • A gateway access token for model requests
  • A separate control-plane bearer token with events:read
  • A model ID configured on that gateway

Create the agent first with kt agent create or the control-plane API, then link and deploy it according to your gateway mode. Keep both credentials out of browser code.

Install the model client

mkdir keeptrusts-agent
cd keeptrusts-agent
npm init -y
npm pkg set type=module
npm install openai dotenv
npm install --save-dev tsx typescript @types/node
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --types node

Configure the runtime

Create .env:

KEEPTRUSTS_API_URL=https://api.keeptrusts.com
KEEPTRUSTS_CONTROL_PLANE_TOKEN=replace-with-control-plane-token
KEEPTRUSTS_GATEWAY_URL=http://localhost:41002/v1
KEEPTRUSTS_GATEWAY_TOKEN=replace-with-gateway-access-token
KEEPTRUSTS_AGENT_ID=replace-with-agent-uuid
KEEPTRUSTS_MODEL=replace-with-a-model-configured-on-this-gateway

The two token variables represent different authorization surfaces. Do not copy one value into both unless your deployment has explicitly issued one credential for both purposes.

Send and correlate a model request

Create src/agent.ts:

import { randomBytes, randomUUID } from "node:crypto";
import OpenAI from "openai";
import "dotenv/config";

function requiredEnv(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`${name} is required`);
return value;
}

const apiUrl = requiredEnv("KEEPTRUSTS_API_URL").replace(/\/+$/, "");
const controlPlaneToken = requiredEnv("KEEPTRUSTS_CONTROL_PLANE_TOKEN");
const gatewayUrl = requiredEnv("KEEPTRUSTS_GATEWAY_URL").replace(/\/+$/, "");
const gatewayToken = requiredEnv("KEEPTRUSTS_GATEWAY_TOKEN");
const agentId = requiredEnv("KEEPTRUSTS_AGENT_ID");
const model = requiredEnv("KEEPTRUSTS_MODEL");

// The gateway records control-plane request IDs as 32 lowercase hex
// characters. Starting with that form keeps the request header and event
// filter identical; it is still a valid UUID value for trail lookup.
const requestId = randomUUID().replaceAll("-", "");
const traceparent = `00-${randomBytes(16).toString("hex")}-${randomBytes(8).toString("hex")}-01`;

const openai = new OpenAI({
baseURL: gatewayUrl,
apiKey: gatewayToken,
defaultHeaders: {
"x-keeptrusts-agent-id": agentId,
"x-request-id": requestId,
traceparent,
},
});

const response = await openai.chat.completions.create({
model,
messages: [{ role: "user", content: "Review this policy exception." }],
});

console.log(response.choices[0]?.message?.content);
console.log({ requestId, traceparent });

const query = new URLSearchParams({
since: "1h",
agent_id: agentId,
request_id: requestId,
limit: "1",
});
const eventResponse = await fetch(`${apiUrl}/v1/events?${query}`, {
headers: { Authorization: `Bearer ${controlPlaneToken}` },
});
if (!eventResponse.ok) {
throw new Error(`Event query failed with HTTP ${eventResponse.status}`);
}

const eventPage = await eventResponse.json() as {
events: Array<{
event_id: string;
request_id: string;
timestamp: string;
verdict: string;
event_attribution: { agent_id?: string };
event_cost_attribution?: { total_cost_usd?: number; currency?: string };
}>;
next_cursor?: string | null;
};

const event = eventPage.events[0];
if (!event) {
throw new Error(
`No event found for request ${requestId}; retain this ID and query again after event delivery`,
);
}
if (event.request_id !== requestId) {
throw new Error(`Event request ID mismatch: expected ${requestId}`);
}
if (event.event_attribution.agent_id !== agentId) {
throw new Error(`Event agent attribution mismatch: expected ${agentId}`);
}
console.log(event.event_id, event.verdict, event.event_cost_attribution);

Run it:

npx tsx src/agent.ts

Verification succeeds when the event request_id equals requestId and the event_attribution.agent_id matches the configured agent. If the first query returns an empty events array, retain the request ID printed by the error, allow time for gateway event delivery, and query /v1/events again with that ID. Rerunning the whole script sends a new request with a new ID.

The 32-character request ID is intentional. The gateway echoes a supplied x-request-id on the model response after trimming it and limiting it to 128 characters, but canonicalizes that normalized value before writing control-plane events:

  • a UUID is stored as lowercase hexadecimal text without hyphens;
  • an existing 32-character lowercase hexadecimal ID is stored unchanged;
  • other text is stored as the first 16 bytes of its SHA-256 digest, encoded as 32 lowercase hexadecimal characters.

GET /v1/events?request_id=... performs an exact text match. Starting with the canonical form avoids having to transform a response header before querying the event.

Why this page does not import the Agent SDK

The 0.1.0 package currently fails during import in an ordinary Node.js process. Its event helpers also expect a response wrapper that differs from the API's { "events": [...] } shape. The example above uses the supported gateway and control-plane contracts directly rather than presenting those helpers as runnable.

Next steps