Hyperstruck
Integrations

REST integration

Wire the learning loop into an agent you already run, in any language, with no SDK and no middleware.

In one line

Four endpoints, one correlation key you invent, and no dependency on our SDK: resolve before the work, then reinforce, observe, or decline after it.

The LangGraph middleware hides this loop because it can see your run start and end. If you are on another framework, another language, or your own orchestration, you drive the same loop directly. Nothing about the corpus is different: a learning earned through REST competes with one earned through the middleware on identical evidence.

The shape of it

            your agent's turn
                    |
   resolve  ------->|            what prior runs learned, before the work
                    |
            (the model does the work)
                    |
   reinforce -----> |            what actually happened, so the offer is credited
      or observe    |            a full trace, when you have one
      or decline    |            nothing worth learning, said out loud

Every call carries two things: agent_name, the human-readable name of the corpus you are reading and writing, and run_id, a correlation key you invent. Reusing one run_id across the pair is the entire attribution mechanism.

agent_name is a name you choose

It is unique within your tenant and it is not the hosted agent UUID used in /agents/{agent_id} paths. If no agent by that name exists, one is created on first use, which needs agents:write: a read-only key meeting an unknown name gets a 403 rather than a new agent. Send the same name every time or your corpus splits in two.

run_id is yours, and it is not a hosted run

GET /runs/{run_id} will not find it. It is an idempotency and attribution key you own. It must also avoid the prefix reserved for corpus distillation, which that page names.

Before the work: resolve

curl -X POST "https://api.hyperstruck.com/resolve" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_name": "support-agent",
    "run_id": "support-agent:ticket-2048:attempt-1",
    "goal": "Triage ticket SUP-2048 and recommend the next action.",
    "available_tools": [
      { "name": "ticket_lookup", "description": "Retrieve a support ticket by identifier." }
    ],
    "source_framework": "custom-api-client"
  }'

Required scope: agents:read, or agents:write if the name is new. The response carries four fields, and both shelves come back in the same call.

FieldWhat to do with it
injected_textThe learnings block. Place it where your model will actually read it.
offered_learning_idsWhat was offered, so the write-back can credit it. Keep them for the turn.
injected_facts_textThe claims block: specific facts about the things this goal names. Rendered separately so you can place or fence it differently.
offered_claim_idsThe same, for facts.

Pass available_tools if you can. A lesson about a tool only reaches runs that can actually use that tool, so omitting them silently narrows what you get back. source_framework attributes the per-host funnel below, so give it a stable value for your integration.

After the work: pick one of three

This is the half that hosts get wrong, and the failure is always the same shape: the run resolves and then nothing arrives. An unclosed run is indistinguishable from a host that stopped writing back, and only you know which it was.

You haveSendWhy
A real execution traceobserveSteps, tools, outcome. New learnings are extracted from it and the offer is credited.
An outcome but no trace worth sendingreinforceCredits or corrects the learnings resolve offered, without extracting new ones.
Nothing worth learning fromdeclineCloses the run honestly. Nothing is added to the corpus.

observe

The episode fields are documented in full on the learning loop page. Required scope: agents:write. Returns 202.

reinforce

curl -X POST "https://api.hyperstruck.com/reinforce" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_name": "support-agent",
    "episode": {
      "run_id": "support-agent:ticket-2048:attempt-1",
      "goal": "Triage ticket SUP-2048 and recommend the next action.",
      "outcome": { "is_success": true }
    },
    "is_delivered": true
  }'

Required scope: agents:write. Returns 202; processing is asynchronous.

Beyond agent_name, the episode carrying your run_id, goal and outcome, and an optional org_id, three fields report what happened to the recall itself, and a fourth governs organization sharing. The three are what turn a write-back into evidence rather than an assertion.

Prop

Type

If you can only send one of these three, send is_delivered. It is what separates a broken integration from a quiet one.

decline

curl -X POST "https://api.hyperstruck.com/decline" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_name": "support-agent",
    "run_id": "support-agent:ticket-2048:attempt-1",
    "reason": "no_tool_calls",
    "is_delivered": true
  }'

Required scope: agents:write. reason is a closed set that grows as new cases are named, currently including no_tool_calls, below_material_threshold, empty_offer, and unevidenced_outcome; take the current members from the schema, since an unrecognised value is rejected rather than stored. A run_id this agent never resolved is accepted and ignored, so you can call it defensively.

Set is_delivered honestly here too. A turn can receive the injection and still have nothing to teach, and the flag is what decides whether that resolve is billed or released.

Check your own loop: funnel

GET /funnel reports the closure funnel per producing host: resolved, offered, observed, reinforced, plus half_open, half_open_rate, loop_closure_rate, in_flight, parked, parked_stalled, and avg_seconds_to_close.

curl -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/funnel"

Required scope: agents:read. Scoped to your tenant, grouped by the source_framework your calls declare, which is the reason to set it.

Two parameters shape the answer. window_hours defaults to 24, and in practice 24 is also the ceiling: the window is narrowed to the retention interval, which resolves to one day in shipped deployments, so a larger request is served at 24 and the response reports the window actually used. grace_minutes defaults to 120 and is how long an asynchronous write-back has before its run counts as half-open, so a run still legitimately in flight is reported as in-flight rather than as a failure. A window shorter than twice the grace period is refused with a 422 rather than answered, because it holds no run old enough to have closed.

A run that resolved and never wrote back shows here as a non-closure, not as a false closure. That is the number to watch after you ship, and it is the only place the failure is visible, because a host that stops writing back reports nothing by definition.

Read parked_stalled beside it before concluding what is wrong. It counts inside half_open and rises when a write-back container dies without running at all, which is a different fault from a live host whose write-back path is dropping turns.

Checklist

  • One agent_name, for the life of the integration.
  • One run_id per turn, reused across the pair.
  • A stable source_framework.
  • Exactly one write-back per resolve.
  • is_delivered, always.
  • half_open_rate after launch.
  • Learning loop for the concept and the LangGraph path
  • Framework integrations for LangGraph, the OpenAI Agents SDK, and CrewAI
  • Distill for documents and post-mortems that were never a run
  • Learnings API for storing, searching, and reinforcing specific lessons by hand
  • Claims for the fact shelf that comes back through the same resolve