Skip to content

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.

recipe.py
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 accepted

The 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-decide

Why 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.