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.
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 acceptedThe 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-decideWhy 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.