Skip to content
LangChain

LangChain

A LangChain agent is one invoke() call. The model loops over your tools inside it, and you get the result when it is finished. So the kslate calls sit outside the agent. You read the record, call invoke, and write what it returned.

run() does the read and the write. Your decide function is where invoke goes.

recipe.py
from kslate import Agent, Client, StalePremiseError, Write, run
from langchain.agents import create_agent
from langchain_core.tools import tool

RECORD = "incidents/inc-28"
client = Client("http://127.0.0.1:8080", api_key="rem-token")


@tool
def restart_deployment(name: str) -> str:
    """Restart a Kubernetes deployment. Returns the kubectl output."""
    return f"deployment.apps/{name} restarted"


executor = create_agent(
    "anthropic:claude-sonnet-5",
    tools=[restart_deployment],
    system_prompt="You carry out one remediation step. Reply with one line saying what you did.",
)


def decide(fields):
    plan = fields["planned_action"]["value"]
    print(f"carrying out: {plan}")
    result = executor.invoke({"messages": [{"role": "user", "content": f"Carry out: {plan}"}]})
    said = result["messages"][-1].content
    return [Write(op="append", field="actions_taken", entry=said, evidence="kubectl output")]


try:
    run(Agent(decide=decide), client, record=RECORD, max_attempts=1)
    print("write accepted")
except StalePremiseError as e:
    for m in e.moved:
        print(f"rejected: {m.field} moved v{m.your_version} -> v{m.current_version} by {m.current_author}")
    print("the tool already ran, so this does not re-decide")

decide receives the record’s fields and returns the writes to make. It never sees a version. run() sends the versions it read as based_on, so the gateway rejects the write if planned_action moved while the model was working.

Run it against the gateway from Install, with a plan already on the record:

carrying out: roll back api deployment
write accepted

The gateway rejects the write when the diagnoser replaces the plan while the model is working:

carrying out: restart payments-api pods
rejected: planned_action moved v1 -> v2 by apikey:diagnoser
the tool already ran, so this does not re-decide

Why max_attempts=1

restart_deployment restarts a deployment. It runs inside invoke, before any kslate write exists. If the gateway then rejects the write and run() called decide again, the model would restart the deployment a second time.

max_attempts=1 calls decide once and raises the rejection instead. What to do next is yours: the restart happened, and the plan it was based on has been replaced. Handling rejections covers the choices. Leave max_attempts at its default only when running your tools twice is harmless.

Giving the agent kslate as tools

You can wrap the kslate calls as LangChain tools and let the model write. Then the model holds the versions, and a rejection comes back to it as a tool message. kslate cannot make it read the record again and decide again. Any MCP client says what that path can and cannot enforce. The file above keeps the writes in code, where the rejection reaches your except block.

Written against langchain 1.4.5, langchain-anthropic 1.7.6 and kslate 0.9.0.