To test, use a Sandbox API key, and see Test in the Sandbox.
agent:enroll and anchor:write for the adapter, and anchor:read for bundle later.
The example agent on this page has no model, so you need no key from a model provider. It has three nodes: one asks for a tool call, one runs the tool, and one pauses for a person to approve. To use your own agent, keep the Sanning parts and replace the graph.
Install the adapter
@langchain/core as a peer dependency. @aws-sdk/client-s3 is the AWS S3 library, which the Sanning SDK uses to reach your bucket but does not install for you. In Python, the adapter ships inside sanning-anchor: the langchain extra pulls in LangChain, and the s3 extra pulls in the AWS S3 library.Set the environment variables
Export the following variables in the terminal where you run the example. Put them in the secret store that your service already uses for production, and never commit them to your code.Replace the following:
API_KEY: your Live API key. To make one, see Make a Live API key.SEED: the 64 hex characters of the seed for your agent’s signing key. To make one, see Make a seed for the signing key.BUCKET: the name of your log store bucket. The SDK creates it on the agent’s first record. To learn what a log store is, see Set up your log store.REGION: the AWS Region of the bucket, such asus-east-1. The SDK can create the bucket only inus-east-1: for another Region, see A bucket in another Region.ACCESS_KEY_IDandSECRET_ACCESS_KEY: the access key ID and the secret access key of your storage keys. For the permissions they need, see The exact AWS permissions.
Start with an agent
The following graph makes one tool call that states a decision, and then pauses for approval. The tool, The graph calls the tool from
record_conclusion, is how the agent states its decision:assess, then pauses in review, where a person approves:Add the adapter, with a case ID
Make an anchorer, which signs your records and sends them to Sanning, and make the handler that you pass to LangChain as a callback.The case ID is the matter that the work is for. Give it to the handler, and every record of the run carries it, so that you can build one evidence pack for the whole case later.The ID reaches Sanning as plain text, and nobody can change or withdraw it later. Use an ID that means nothing outside your systems: no names, no paths, no personal data. To find a case by customer, keep that link in your own systems.Replace
claims-triage with the agent ID, the name that identifies your agent to Sanning, and keep it stable: it is how the agent shows on the Agents page. Replace case-4b19de with the ID of the case from your own system. A case ID is 1 to 128 characters of letters, digits, _, ., : and -.The log store keeps the content of each step in your S3 bucket, and Sanning never receives it. For why the anchorer uses MemoryStore, see Create the anchorer, and for why you keep the content, see Keep your content.The mode (Sandbox or Live) comes from the start of your API key, so you set nothing else.Record decisions from a named tool
A framework records tool calls, and it cannot tell which call is a decision. Put each decision in a tool, and name that tool to the adapter. When the tool ends without an error, the adapter writes a Decision record with the name that you give it. The value is what the tool returned, and it is hashed with the record like any other content.Name the decision for what was decided, and put the outcome in the value. The name Pass the tool object. The handler refuses a tool that does not exist, a tool named twice, and a name that is not a valid label (1 to 128 characters, with no control characters), when you construct it. A call that ends with an error writes no decision.
claim_verdict with the value declined is right. The name claim_declined is not: a name that holds one outcome stops a reviewer from comparing decisions.Record approvals
When a person must approve a step, use a LangGraph
interrupt(), as the review node does. An interrupt does not say who decides, so tell the adapter, and tell it how to read the answer. The adapter then writes an Approval requested record when the node pauses, and an Approval resolved record when the run resumes.actor is human, policy or model. verdict returns approved, rejected or edited, and it must raise an error for a value that is not an answer. Only a LangGraph resume command answers a pending approval.The two records are in different runs, and a LangGraph thread_id ties them together. A thread_id is 1 to 128 characters of letters, digits, _, ., : and -. Use an ID that means nothing outside your systems: the adapter refuses every record of a run whose thread_id is an email address, for example.The request waits in the process until the run resumes. If the answer can arrive in another process, pass a store that outlives the first process as approvals.pending (pending in Python). Without approvals, the adapter records no approvals, and the first record of each run says so.Close the run, and check for gaps
You must close the run in every case. A run with no closing record cannot show how it ended, and a reader cannot tell it from a process that died. The closing record holds the outcome you state and the outcome the adapter observed, so if they differ, a reviewer sees both.The closing record also lists the records that the run wrote. A step that could not be recorded is a gap, and When the run ends, close it with its outcome and check for gaps. Then close the anchorer:
close() reports it. Check for gaps when you close, because the agent keeps running by default when a record is lost.outcome is completed, failed, interrupted or unknown. reason is a code: error, timeout, cancelled, interrupted or policy. State the outcome on the first close(): a later one does not rewrite the closing records.Run it, and see the records
Put the pieces in one file: Open the console and go to Records. Each row is a record from your run: the run starts, the steps and the tool call run, the decision is stated, and the approval is requested and then resolved. The run ends with a closing record.The Records page does not show whether a record is stamped. To find out, run
agent.mts for TypeScript or agent.py for Python. The whole file is at the end of this page. Run it in the terminal where you exported the environment variables.bundle, as in Wait for the stamp.If a step must not run unrecorded
By default, a lost record does not stop the agent. If a step must not run unless its record is kept, build the anchorer withfailClosed. The handler then waits for each step’s records and stops the agent when one is lost, before the next step runs. A tool whose start record is lost does not run.
What the adapter cannot record
The first record of each run lists what the adapter does not capture: for example, approvals when you do not passapprovals, and decisions when you name no decision tool. The record types, and which LangChain event writes each, are in How adapters work.
The whole file
TypeScript: agent.mts
TypeScript: agent.mts
Python: agent.py
Python: agent.py