CrewAI
A CrewAI crew runs to completion in one kickoff() call. However many
agents and tasks it holds, you get one result at the end. So the kslate
calls sit outside the crew. You read the record, call kickoff(), and
write what it returned.
CrewAI and kslate both export a class called Agent. Import kslate as a
module to keep the two apart.
import kslate
from crewai import LLM, Agent, Crew, Task
from crewai.tools import tool
RECORD = "incidents/inc-32"
client = kslate.Client("http://127.0.0.1:8080", api_key="rem-token")
@tool("Restart deployment")
def restart_deployment(name: str) -> str:
"""Restart a Kubernetes deployment. Returns the kubectl output."""
return f"deployment.apps/{name} restarted"
remediator = Agent(
role="Remediator",
goal="Carry out one remediation step and say what you did",
backstory="You run one kubectl command and report the result in one line.",
tools=[restart_deployment],
llm=LLM(model="anthropic/claude-sonnet-5"),
)
def decide(fields):
plan = fields["planned_action"]["value"]
print(f"carrying out: {plan}")
task = Task(description=f"Carry out: {plan}", expected_output="one line", agent=remediator)
out = Crew(agents=[remediator], tasks=[task]).kickoff()
return [kslate.Write(op="append", field="actions_taken", entry=str(out), evidence="crew output")]
try:
kslate.run(kslate.Agent(decide=decide), client, record=RECORD, max_attempts=1)
print("write accepted")
except kslate.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 crew already ran, so this does not re-decide")decide receives the record’s fields and returns the writes to make.
run() sends the versions it read as based_on, so the gateway rejects
the write if planned_action moved while the crew 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 crew is working:
carrying out: restart payments-api pods
rejected: planned_action moved v1 -> v2 by apikey:diagnoser
the crew already ran, so this does not re-decideWhy max_attempts=1
restart_deployment restarts a deployment. The crew calls it while it
runs, before any kslate write exists. If the gateway rejects the write
and run() called decide again, the crew would restart the deployment
a second time.
max_attempts=1 calls decide once and raises the rejection instead.
Handling
rejections
covers what to do with it. Leave max_attempts at its default only when
running the crew twice is harmless.
One crew, one writer
The gateway sees one identity here: the API key the client holds. Every field the crew writes is written by the remediator role, whichever crew member decided it. If you want each crew member’s writes attributed separately, give each one its own client with its own credential and its own role. Scope and identity has the shape of those roles.
Written against crewai 1.15.27 and kslate 0.9.0.