Integration Guide
Wrap your agent loop with three SDK calls. Every run declares a boundary — which tools it may call, how destructive a write may be, a loop threshold, a budget, a step cap, a timeout — checked atomically before every call fires.
Three calls. Every limit enforced.
Drop into any agent loop — LangGraph, CrewAI, OpenAI Agents SDK, or your own.
Install the SDK
npm install @thsky-21/thskyshieldAdd credentials
THSKYSHIELD_SITE_ID=your_site_id_here
THSKYSHIELD_KEY=your_api_key_hereWrap your agent loop
import { Thskyshield, ShieldKilledError } from '@thsky-21/thskyshield'
const shield = new Thskyshield({
siteId: process.env.THSKYSHIELD_SITE_ID!,
apiKey: process.env.THSKYSHIELD_KEY!,
})
const run = await shield.beginRun({
budgetLimitUsd: 2.00, // hard ceiling — run dies here
iterationLimit: 30, // max steps
timeoutSeconds: 300, // wall-clock timeout
loopThreshold: 5, // kill after same prompt repeats N times
allowedTools: ['search', 'read_file'], // the only tools this run may call
maxMutationClass: 'reversible', // no destructive writes
})
try {
while (!done) {
// Gate fires before the call. Throws if any boundary is hit — tools,
// writes, loop, budget, steps, or time.
const { requestId, remainingUsd } = await run.beforeStep({
stepType: 'llm',
model: 'gpt-4o-mini',
estimatedTokens: { input: 500, output: 200 },
promptInput: currentPrompt, // used for loop detection
})
const result = await callYourLLM(currentPrompt)
// Settle actual cost after the call resolves.
await run.afterStep({
requestId,
actualTokens: result.usage,
model: 'gpt-4o-mini',
})
}
} catch (e) {
if (e instanceof ShieldKilledError) {
// e.reason: 'killed_scope_tool' | 'killed_scope_mutation' | 'killed_loop'
// | 'killed_timeout' | 'killed_iterations' | 'killed_budget'
console.log(`Agent stopped: ${e.reason}. Spent: $${e.spent}`)
}
} finally {
const summary = await run.end()
console.log(`Total: $${summary.totalCostUsd} over ${summary.iterationCount} steps`)
}Always call end() in finally
If your agent throws before end() is called, the run stays open in Redis until TTL expiry. The finally block ensures cleanup on both success and error paths.
Prefer the auto-managed wrapper?
// withRun() manages beginRun / end automatically
const { result, summary } = await shield.withRun(
{ budgetLimitUsd: 1.00, iterationLimit: 20 },
async (run) => {
// your agent loop here — same beforeStep / afterStep pattern
return finalResult
}
)withRun() calls beginRun() and end()for you — useful when you don't need to inspect the run handle directly.
Methods
shield.beginRun({ budgetLimitUsd, iterationLimit?, timeoutSeconds?, loopThreshold?, allowedTools?, maxMutationClass?, strictScope?, externalRunId? })InitCreates a governed agent run and declares its boundary — writes a run record to Supabase and initialises all Redis state atomically. budgetLimitUsd is required — the run is killed the moment accumulated spend would exceed it. allowedTools (max 100 names) and maxMutationClass ('read_only' | 'reversible' | 'destructive') scope which tools may be called and how destructive a write may be; omit either and that gate is skipped. All other limits have defaults (iterationLimit: 50, timeoutSeconds: 300, loopThreshold: 5). Returns a Run handle — keep this reference for the duration of the agent loop.
run.beforeStep({ stepType, model?, estimatedTokens?, promptInput?, toolName?, mutationClass? })BeforeSingle atomic Redis round-trip that checks the run's whole boundary — tools, writes, loop, budget, iterations, and timeout — before the call fires. Throws ShieldKilledError immediately if any of them is hit. On success, reserves the estimated cost and returns a requestId — pass this to afterStep() to link the reservation to actual spend. promptInput is SHA-256 fingerprinted for loop detection; toolName / mutationClass are checked against the allowedTools / maxMutationClass declared in beginRun. Pass what applies to the step you're making — a 'tool' step should carry toolName and mutationClass, an 'llm' step promptInput.
run.afterStep({ requestId, actualTokens, model? })AfterSettles the cost reservation atomically. Releases the estimated reservation written by beforeStep() and applies the actual token cost. Must be awaited — fire-and-forget breaks the two-phase accounting and can let the next beforeStep() see a stale budget.
run.end()CloseCloses the run. Reads final Redis state and writes a permanent record to Supabase. Always call in a finally {} block — if the agent throws before end() is called, the run remains open in Redis until TTL expiry. Returns the full run summary including total cost and kill reason if applicable.
shield.withRun(options, async (run) => T)HelperConvenience wrapper that calls beginRun(), runs your async function, then calls end() automatically — even on throw. Use this instead of the manual try/finally pattern when you don't need fine-grained control over the run lifecycle.
ShieldKilledError
Thrown by beforeStep() when a kill condition is met. Catch it to handle the kill gracefully — access partial results before the run ends.
| Property | Type | Description |
|---|---|---|
.reason | string | Kill status — one of the kill codes below. |
.runId | string | The run ID that was killed. |
.spent | string | USD spent before the kill, e.g. '1.84'. |
.remaining | string | USD remaining at kill time. |
Two kinds of gate, enforced differently
allowedTools / maxMutationClassand the loop threshold are containment gates — live the moment a project's switched to enforce. Budget, iterations and timeout are measured from the first run and enforced once you turn that on for the project too, so an early integration can watch its resource usage before it starts stopping runs over it.
Any stack. Same enforcement.
All five routes accept Authorization: Bearer YOUR_API_KEY and return JSON. The SDK is a thin wrapper over these endpoints.
# 1 — Begin a run, with a declared boundary
curl -X POST https://thskyshield.com/v1/run/begin \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"budgetLimitUsd": 2.00,
"iterationLimit": 30,
"timeoutSeconds": 300,
"loopThreshold": 5,
"allowedTools": ["search", "read_file"],
"maxMutationClass": "reversible"
}'
# → { "runId": "run_abc123", "status": "running", "startedAt": "..." }
# 2 — Gate before each step
curl -X POST https://thskyshield.com/v1/run/run_abc123/before-step \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"stepType": "llm",
"model": "gpt-4o-mini",
"estimatedTokens": { "input": 500, "output": 200 },
"promptInput": "Summarize this document..."
}'
# → { "allowed": true, "requestId": "req_xyz", "remainingUsd": "1.94" }
# 2b — Or a tool step, checked against the boundary declared in step 1
curl -X POST https://thskyshield.com/v1/run/run_abc123/before-step \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"stepType": "tool",
"toolName": "search",
"mutationClass": "read_only"
}'
# → 429, { "allowed": false, "reason": "killed_scope_tool" } if toolName isn'''t in
# allowedTools, or "killed_scope_mutation" if mutationClass exceeds maxMutationClass
# 3 — Settle after the LLM responds
curl -X POST https://thskyshield.com/v1/run/run_abc123/after-step \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"requestId": "req_xyz",
"actualTokens": { "input": 487, "output": 193 },
"model": "gpt-4o-mini"
}'
# 4 — Close the run
curl -X POST https://thskyshield.com/v1/run/run_abc123/end \
-H "Authorization: Bearer YOUR_API_KEY"
# → { "status": "completed", "totalCostUsd": "0.06", "iterationCount": 8 }
# Read run state at any time
curl https://thskyshield.com/v1/run/run_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"Kill Codes
The value of ShieldKilledError.reason and the status field on the run record, in the order the engine actually checks them — a run stops on whichever trips first.
| Code | Trigger |
|---|---|
killed_scope_tool | toolName isn't in the run's allowedTools |
killed_scope_mutation | mutationClass exceeds the run's maxMutationClass |
killed_loop | same prompt fingerprint seen ≥ loopThreshold times within 600s |
killed_timeout | elapsed time since beginRun() exceeds timeoutSeconds |
killed_iterations | iteration counter ≥ iterationLimit |
killed_budget | spent + reserved + estimated cost exceeds budgetLimitUsd |
SYSTEM_DEGRADED | control plane error (Redis unreachable, etc.) |
Want help with your integration?
We wire the SDK into your agent loop with you — 30 minutes, no cost to try it.
Talk to us