Written by the Cuttlely team. Cuttlely is an open-source, Apache-2.0 agent orchestration platform. The example below uses plain Python and stub agents. It doesn't require Cuttlely or an API key.
Agent A spots the problem. "Don't touch the production database." Good. It writes that down and hands the job to agent B.
Agent B reads a summary. The summary says something like "be careful with prod." Agent C gets a summary of that. By the time agent D picks up the work, the rule's still in there, technically. It's just stopped meaning anything.
You can reproduce that failure without a model: drop one constraint during a handoff, or keep its ID while changing what it forbids. The example below does both. It demonstrates checks for those cases, not a measured failure rate for real agent systems.
Why the rule fades
Summarizers compress. That's their job. A "must" is short, sounds like context, and gets folded into the paragraph around it. Once it's context, the next model weighs it against the task. A receiving model may interpret a softened rule differently.
There's a quieter version too. Someone asks you to investigate all the failed payments. Hop two narrows it to "a few representative cases." Every agent after that does the narrowed version perfectly. Nothing errors. The scope just shrank on the way down.
So the fix can't be "write the constraint more forcefully." You need checks that don't depend on any model remembering anything.
Stop letting the summary carry the "must"
Keep hard stops as structured records the orchestrator owns. Each one gets an ID, the action it forbids, and who set it. Pass that block next to the handoff, copied byte for byte, never through a summarizer. The model can summarize the task all it wants. It doesn't get to touch the constraint block.
Same trick works for scope: quote the person's original request verbatim first, then add the summary. A summary can narrow what they asked. Their exact words can't, and it's trivial to check later whether the quote's there.
Three checks that don't need model judgment
1. Echo the IDs back and diff them. Before the receiving agent starts, it lists the constraint IDs it's bound by. The orchestrator diffs that against what it sent. A missing ID fails the handoff right there. No vibes involved.
2. Hash the constraint block at every hop. ID echo catches drops. It won't catch an agent that keeps C1 but rewrites "never delete records" into "don't delete records unless you're sure." So the orchestrator hashes the original block once and recomputes it at every hop. Any edit, even one softened word, flips the hash.
3. Enforce at the tool call, against the original list. Even with intact handoffs, the actual gate belongs in code. Every tool call gets checked against the orchestrator's original constraint list. The last agent's version doesn't count. Then it stops mattering whether agent D "remembers" the rule.
These three checks are an application pattern you can add around any dispatcher, not a demonstration of Cuttlely's built-in safeguards.
The runnable version
Save the complete example below as handoffs-example.py. It uses stub agents: one passes the constraints through, one drops a constraint, and one changes a rule.
"""Catch dropped constraints at every agent handoff. No API key, stub agents. Run: python3 handoffs-example.py""" import hashlib, json CONSTRAINTS = [ {"id": "C1", "forbids": "delete_records", "set_by": "orchestrator"}, {"id": "C2", "forbids": "email_customer", "set_by": "orchestrator"}, {"id": "C3", "forbids": "deploy_prod", "set_by": "orchestrator"}, ] def digest(block): return hashlib.sha256(json.dumps(block, sort_keys=True).encode()).hexdigest() class HandoffError(Exception): pass # Stub agents. Each "summarizes" the handoff for the next one. def faithful_agent(h): return {"task": h["task"], "constraints": h["constraints"]} def lossy_agent(h): # Summarizer quietly drops the last constraint. Looks harmless. return {"task": h["task"], "constraints": h["constraints"][:-1]} def editing_agent(h): # Keeps all three IDs but softens one. ID echo alone won't see this. c = [dict(x) for x in h["constraints"]] c[0]["forbids"] = "delete_records_unless_sure" return {"task": h["task"], "constraints": c} class Orchestrator: def __init__(self, constraints): self.original = [dict(c) for c in constraints] self.ids = {c["id"] for c in constraints} self.hash = digest(constraints) def check_hop(self, hop, handoff): echoed = {c["id"] for c in handoff["constraints"]} missing = self.ids - echoed if missing: raise HandoffError(f"hop {hop}: missing constraint ids {sorted(missing)}") if digest(handoff["constraints"]) != self.hash: raise HandoffError(f"hop {hop}: constraint block hash changed") def run(self, chain, task): h = {"task": task, "constraints": self.original} for hop, agent in enumerate(chain, 1): h = agent(h) self.check_hop(hop, h) return h def guard_tool_call(self, tool): # Enforce against the ORIGINAL list, never the last agent's copy. for c in self.original: if c["forbids"] == tool: raise PermissionError(f"{tool} blocked by {c['id']}") return f"{tool} ok" def scenario(name, chain): o = Orchestrator(CONSTRAINTS) try: o.run(chain, "clean up stale accounts") print(f"[PASS] {name}: constraints intact") except HandoffError as e: print(f"[CAUGHT] {name}: {e}") if __name__ == "__main__": scenario("3 faithful hops", [faithful_agent] * 3) scenario("lossy summarizer at hop 2", [faithful_agent, lossy_agent, faithful_agent]) scenario("silent edit at hop 3", [faithful_agent, faithful_agent, editing_agent]) # The CI-style test: the blocker has to fire. o = Orchestrator(CONSTRAINTS) try: o.guard_tool_call("delete_records") raise SystemExit("[FAIL] forbidden call went through, enforcement is broken") except PermissionError as e: print(f"[PASS] blocker fired: {e}") print(f"[PASS] allowed call: {o.guard_tool_call('read_records')}") Run the saved file with python3 handoffs-example.py. The expected output is:
$ python3 handoffs-example.py [PASS] 3 faithful hops: constraints intact [CAUGHT] lossy summarizer at hop 2: hop 2: missing constraint ids ['C3'] [CAUGHT] silent edit at hop 3: hop 3: constraint block hash changed [PASS] blocker fired: delete_records blocked by C1 [PASS] allowed call: read_records ok The lossy summarizer gets caught by the ID diff. The sneaky edit sails past the ID check, since all three IDs are still there, and the hash nails it. The hash detects changes to the serialized block; the ID diff identifies missing rules. This example shallow-copies each constraint dict into self.original, so an in-place edit of the caller's list cannot change the orchestrator's top-level records. Nested mutable values would still be shared. The handoff still passes that list by reference, so it is not an isolation boundary against agents that mutate shared objects. Treat the sample as an illustration, not production enforcement. A real dispatcher must protect its original records and check every execution path.
In a real system the echo comes from the model, so you'd parse its list of IDs out of a structured response and diff that. The hash runs on whatever block arrives with the handoff. Neither check asks a model to grade anything.
Make the blocker prove it fires
Here's the bit people skip. You wrote a guard. Cool. Does it work?
Build a handoff where a blocker should fire. Run it through the whole chain. Assert the forbidden tool call gets refused. If that call ever goes through, the test fails and your build goes red.
def test_blocker_fires(): o = Orchestrator(CONSTRAINTS) o.run([faithful_agent] * 3, "clean up stale accounts") try: o.guard_tool_call("delete_records") except PermissionError: return raise AssertionError("forbidden call went through; enforcement is broken") Keep the allowed-call check from the full example too. A guard that refuses everything would otherwise pass the refusal test. A pipeline that never blocks anything passes every happy-path test you'll ever write. Exercise the refusal path explicitly. Put it in CI and run it every time, because the next refactor of your handoff format is exactly the change that breaks it.
About "confirm you would do X"
A popular pattern is embedding a test case in the prompt: "this constraint overrides all others and must produce action X when Y is true. Confirm you would do X."
It's better than nothing. It'll catch some drops early. But it's still the model grading itself, in the same context it's about to act in. It can say yes and then drift three steps later. Keep it as an early warning if you like. Don't let it be the thing standing between the agent and the action.
Same logic applies to reviewers. A reviewer sharing the actor's context tends to approve its own reasoning. Deterministic checks like tests, hashes and set diffs are the part of the chain that doesn't paraphrase.
Some rules won't fit any of this. "Don't deploy if you're unsure" isn't a checkable action. Those still need a human approval step before anything destructive or hard to undo.
Put the checks in a customer-care workflow
The customer-care demo documented in Cuttlely 1.6.1 follows a late order. A customer specialist reads the account, an operations specialist finds the parcel never left the warehouse, and a policy specialist checks the written policy after operations finishes. Harness Prime writes the decision, reply and actions with the evidence.
That gives you a concrete workflow to reason about. Suppose your application forbids sending a customer email until a human approves it. Keep that rule in the application's protected records, preserve it through the specialist handoffs, and enforce it before an email tool executes. That's a proposed application rule, not a claim that the demo implements this guard or the Python checks above.
The existing demo guide opens the team using your own model key. The Python example above uses no model and proves nothing about a live Cuttlely run.
Try it
Save the example and adapt its checks to your own dispatch path. To explore Cuttlely, start with the quickstart. If it helps your work, star the repository so other developers can find it.

