Agent SDK · Documentation

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.

Quick start

Three calls. Every limit enforced.

Drop into any agent loop — LangGraph, CrewAI, OpenAI Agents SDK, or your own.

1

Install the SDK

terminal
npm install @thsky-21/thskyshield
2

Add credentials

.env.local
THSKYSHIELD_SITE_ID=your_site_id_here
THSKYSHIELD_KEY=your_api_key_here
3

Wrap your agent loop

agent/run.ts
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?

agent/run.ts
// 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.

SDK Reference

Methods

shield.beginRun({ budgetLimitUsd, iterationLimit?, timeoutSeconds?, loopThreshold?, allowedTools?, maxMutationClass?, strictScope?, externalRunId? })Init
Promise<Run>

Creates 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? })Before
Promise<{ requestId: string, remainingUsd: string }>

Single 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? })After
Promise<{ success: boolean, actualCostUsd: string }>

Settles 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()Close
Promise<{ runId, status, totalCostUsd, iterationCount, killedReason? }>

Closes 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)Helper
Promise<{ result: T, summary: RunSummary }>

Convenience 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.

PropertyTypeDescription
.reasonstringKill status — one of the kill codes below.
.runIdstringThe run ID that was killed.
.spentstringUSD spent before the kill, e.g. '1.84'.
.remainingstringUSD 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.

HTTP API — any language

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.

POST /v1/run/beginPOST /v1/run/{id}/before-stepPOST /v1/run/{id}/after-stepPOST /v1/run/{id}/endGET /v1/run/{id}
terminal
# 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

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.

CodeTrigger
killed_scope_tooltoolName isn't in the run's allowedTools
killed_scope_mutationmutationClass exceeds the run's maxMutationClass
killed_loopsame prompt fingerprint seen ≥ loopThreshold times within 600s
killed_timeoutelapsed time since beginRun() exceeds timeoutSeconds
killed_iterationsiteration counter ≥ iterationLimit
killed_budgetspent + reserved + estimated cost exceeds budgetLimitUsd
SYSTEM_DEGRADEDcontrol plane error (Redis unreachable, etc.)
Get help

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