Design the escalation path for a working agent: trigger conditions for stopping, severity tiers, the fields a complete escalation packet carries, routing, waiting behavior, and resume conditions, with a check script that validates a packet before it is sent. Use when an agent run needs a defined way to stop and hand off when blocked, uncertain, or facing harm or cost beyond its brief.
./install.sh agent-escalation-designname: agent_escalation_design description: "Design the escalation path for a working agent: trigger conditions for stopping, severity tiers, the fields a complete escalation packet carries, routing, waiting behavior, and resume conditions, with a check script that validates a packet before it is sent. Use when an agent run needs a defined way to stop and hand off when blocked, uncertain, or facing harm or cost beyond its brief." metadata: { "includeInPrompt": true }
Design an escalation path before a run needs one: the conditions that stop work, the severity tier that sets the response, the packet that carries the case, the recipient that receives it, the behavior while waiting, and the condition that allows work to resume. Without a path set in advance, a blocked run either pushes ahead past its brief or stops with a note too thin for anyone to act on. This skill turns the stop into a checked handoff with named fields and a pass or fail result from a fixed check routine.
Scope:
python3 --version returned on the drafting machine. The script uses only the Python standard library (argparse, json, and sys), so no package version is quoted and no install step is needed. Every number in the Output Contract and in the expected output blocks comes from a real run of that script against its fixtures.blocked: a required input, tool, or record is unavailable and no workaround inside the brief exists.uncertain: two or more readings of the task or the data remain plausible after checking, and picking wrong carries real cost.harm_risk: the next step could damage data, property, accounts, or third parties, or could reverse work that is hard to undo.cost_risk: the next step would spend money, quota, or time past the limit stated in the brief.permission_denied: access the task needs was refused, expired, or was never granted.conflicting_instructions: two instructions in force point at different actions and no priority rule in the brief settles them.missing_input: a fact the task depends on is absent and cannot be derived from records the run can read.tool_failure: a tool fails repeatedly in a way that changes the result, after at least one retry with a different approach.low: work can continue on other parts of the task, and a decision is needed before the blocked part is finished.medium: the blocked part cannot finish without a decision, and other parts may continue under the waiting rules in step 8.high: the whole run stops, because continuing any part risks building on a wrong or unsafe base.critical: the run stops at once and any partial action in progress is made safe first, because harm, loss, or an irreversible step is close.medium at most. A run that could still act, where acting wrong is costly, is high or critical.critical packet requires waiting behavior paused or stopped. The check script enforces this rule, because a critical case that keeps doing read or subset work has no clear stop line.attempted is a list of short plain statements, one per attempt, in the order tried. Each entry names the action and the result, for example a retry that returned the same error or a check that ruled out one reading.attempted list as an error.packet_id: a stable identifier for this packet, unique within the run record.run_id: the identifier of the run that escalated, as it appears in the run record.task_summary: one or two plain sentences naming the task and the goal.severity: one tier from step 2.trigger_type: one trigger from step 1.trigger_detail: the concrete facts that fired the trigger, with numbers, names, or error text copied from the run record, not paraphrased from memory.attempted: the ordered list from step 4.evidence: the list from step 6.completed: a list of the parts of the task that are done and verified.remaining: a list of the parts still to do, including the blocked part.blocked_on: the single point the run is stopped on, stated as one sentence.requested_action: the exact decision or input needed, stated so a reply can give it in one line, for example pick value A or value B, approve a stated spend, or name the authoritative record.recipient: the queue, role, system, or agent set in step 7.waiting_behavior: one behavior from step 8.resume_condition: the condition from step 10, stated as an observable event, for example a reply naming a value, not a vague hope that the case gets resolved.blocked_on and requested_action separate. The first states the problem point. The second states the decision that clears it. A packet that merges them forces the recipient to guess what reply would be enough.kind and ref. Use only four kinds:link: a URL the recipient can open to see the record.artifact: an identifier for a produced file, export, or other output the recipient can retrieve.test: an identifier for a named check or run with a recorded result.log: an identifier for a run record or transcript that shows the work or the failure.high and critical packets, two entries are advised, and the script adds a warning when a high severity packet carries only one.paused: no further task work until the resume condition is met. Status checks and packet delivery retries are allowed.read_only: the run may read, search, and verify records, and may prepare drafts or plans, and may not change stored state or spend.safe_subset: the run may continue only the parts listed as unblocked in the design or in the packet, within the brief, and must stop at any point that touches the blocked question.stopped: the run ends. Its state, packet, and records are handed over as they stand, and any later work starts as a new run that cites the packet.safe_subset, list the exact parts that may continue in the packet or the run record. A general claim that other work is safe is not a list, and any part not listed waits.python3 packet_check.py packet.json.resume_condition in the packet is met and the reply or event that met it is recorded: who or what decided, what was decided, and the record that carries it. A reply that comments on the case without deciding the requested_action point does not meet the condition.Deliver the escalation design plus the checked packet and a short case summary, with these sections, in this order:
Run: python3 packet_check.py --make-fixture reproduces the incomplete worked example. Run: python3 packet_check.py --make-valid reproduces the valid worked example. Run it on a real packet with python3 packet_check.py packet.json.
Expected output for the fixture run:
text
packet_id: esc-2026-10-16-004
run_id: run-8841
severity: high
trigger_type: blocked
waiting_behavior: paused
attempted: 1
completed: 2
remaining: 2
evidence_entries: 0
errors: 2
error: requested_action: missing or empty
error: evidence: missing, not a list, or empty
warnings: 0
verdict: FAIL
Expected output for the valid run:
text
packet_id: esc-2026-10-16-005
run_id: run-8842
severity: medium
trigger_type: uncertain
waiting_behavior: read_only
attempted: 2
completed: 2
remaining: 2
evidence_entries: 2
errors: 0
warnings: 0
verdict: PASS
The incomplete fixture is built to trip two rules: its requested_action is an empty string, and its evidence list is empty. All other fixture fields are present and valid, with attempted 1, completed 2, and remaining 2. The valid fixture carries the same task with a filled requested_action and 2 evidence entries, and passes with 0 errors.
packet_check.py (uses only the Python standard library). Save this code as packet_check.py:
python
#!/usr/bin/env python3
"""Check an agent escalation packet for completeness.
Usage:
python3 packet_check.py packet.json
python3 packet_check.py --make-fixture
python3 packet_check.py --make-valid
The packet is a JSON object with these top level fields:
packet_id (string), run_id (string), task_summary (string),
severity (string), trigger_type (string), trigger_detail (string),
attempted (list of strings), evidence (list of objects),
completed (list of strings), remaining (list of strings),
blocked_on (string), requested_action (string),
recipient (string), waiting_behavior (string),
resume_condition (string).
Severity must be one of: low, medium, high, critical.
Trigger type must be one of: blocked, uncertain, harm_risk,
cost_risk, permission_denied, conflicting_instructions,
missing_input, tool_failure.
Waiting behavior must be one of: paused, read_only,
safe_subset, stopped.
Each evidence entry needs kind and ref.
Kind must be one of: link, artifact, test, log.
Exit code is 0 when there are no errors, 1 when there is
at least one error. Warnings do not change the exit code.
"""
import argparse
import json
import sys
SEVERITIES = {"low", "medium", "high", "critical"}
TRIGGERS = {"blocked", "uncertain", "harm_risk", "cost_risk",
"permission_denied", "conflicting_instructions",
"missing_input", "tool_failure"}
WAITING = {"paused", "read_only", "safe_subset", "stopped"}
KINDS = {"link", "artifact", "test", "log"}
REQUIRED_STRINGS = ["packet_id", "run_id", "task_summary",
"trigger_detail", "blocked_on",
"requested_action", "recipient",
"resume_condition"]
FIXTURE = {
"packet_id": "esc-2026-10-16-004",
"run_id": "run-8841",
"task_summary": "Migrate 120 records and verify totals match source.",
"severity": "high",
"trigger_type": "blocked",
"trigger_detail": "Source totals disagree after step 3, difference is 14 records.",
"attempted": ["Re-ran the count query with a fixed filter."],
"evidence": [],
"completed": ["Read source file.", "Loaded 106 records."],
"remaining": ["Load remaining records.", "Verify totals."],
"blocked_on": "Which total is authoritative, source or target.",
"requested_action": "",
"recipient": "review-queue",
"waiting_behavior": "paused",
"resume_condition": "A reply naming the authoritative total."
}
VALID = {
"packet_id": "esc-2026-10-16-005",
"run_id": "run-8842",
"task_summary": "Migrate 120 records and verify totals match source.",
"severity": "medium",
"trigger_type": "uncertain",
"trigger_detail": "Two records share an identifier with different values.",
"attempted": ["Checked both records against the source export.",
"Searched the run log for a prior decision on duplicates."],
"evidence": [{"kind": "log", "ref": "run-8842-step-2"},
{"kind": "artifact", "ref": "export-2026-10-16.csv"}],
"completed": ["Read source file.", "Loaded 118 records."],
"remaining": ["Resolve the duplicate pair.", "Verify totals."],
"blocked_on": "Which value wins for the duplicate identifier.",
"requested_action": "Pick value A or value B for the duplicate identifier.",
"recipient": "review-queue",
"waiting_behavior": "read_only",
"resume_condition": "A reply naming the winning value."
}
def check_str_list(value, label, errors, allow_empty=True):
if not isinstance(value, list):
errors.append(f"{label}: missing or not a list")
return 0
if not allow_empty and len(value) == 0:
errors.append(f"{label}: empty list")
return 0
good = 0
for item in value:
if not isinstance(item, str) or not item.strip():
errors.append(f"{label}: entry is empty or not a string")
else:
good += 1
return good
def check_packet(packet):
errors = []
warnings = []
if not isinstance(packet, dict):
return ["packet is not a JSON object"], warnings, {}
for field in REQUIRED_STRINGS:
val = packet.get(field)
if not isinstance(val, str) or not val.strip():
errors.append(f"{field}: missing or empty")
severity = packet.get("severity")
if severity not in SEVERITIES:
errors.append(f"severity: bad value {severity!r}")
severity_out = "-"
else:
severity_out = severity
trigger = packet.get("trigger_type")
if trigger not in TRIGGERS:
errors.append(f"trigger_type: bad value {trigger!r}")
trigger_out = "-"
else:
trigger_out = trigger
waiting = packet.get("waiting_behavior")
if waiting not in WAITING:
errors.append(f"waiting_behavior: bad value {waiting!r}")
waiting_out = "-"
else:
waiting_out = waiting
attempted_n = check_str_list(packet.get("attempted"), "attempted", errors, allow_empty=False)
completed_n = check_str_list(packet.get("completed"), "completed", errors, allow_empty=True)
remaining_n = check_str_list(packet.get("remaining"), "remaining", errors, allow_empty=True)
if isinstance(packet.get("remaining"), list) and len(packet.get("remaining")) == 0:
warnings.append("remaining: empty list, no remaining work stated")
evidence = packet.get("evidence")
evidence_total = 0
if not isinstance(evidence, list) or len(evidence) == 0:
errors.append("evidence: missing, not a list, or empty")
evidence = []
else:
for entry in evidence:
if not isinstance(entry, dict):
errors.append("evidence: entry is not an object")
continue
kind = entry.get("kind")
ref = entry.get("ref")
if kind not in KINDS:
errors.append(f"evidence: bad kind {kind!r}")
continue
if not isinstance(ref, str) or not ref.strip():
errors.append("evidence: ref is empty")
continue
evidence_total += 1
if severity in ("high", "critical") and evidence_total < 2 and isinstance(evidence, list) and len(evidence) > 0:
warnings.append(f"severity {severity}: only {evidence_total} evidence entry, 2 advised")
if severity == "critical" and waiting not in ("paused", "stopped"):
errors.append("severity critical: waiting_behavior must be paused or stopped")
stats = {
"packet_id": packet.get("packet_id") if isinstance(packet.get("packet_id"), str) and packet.get("packet_id").strip() else "-",
"run_id": packet.get("run_id") if isinstance(packet.get("run_id"), str) and packet.get("run_id").strip() else "-",
"severity": severity_out,
"trigger_type": trigger_out,
"waiting_behavior": waiting_out,
"attempted": attempted_n,
"completed": completed_n,
"remaining": remaining_n,
"evidence_total": evidence_total,
}
return errors, warnings, stats
def print_report(errors, warnings, stats):
print(f"packet_id: {stats.get('packet_id', '-')}")
print(f"run_id: {stats.get('run_id', '-')}")
print(f"severity: {stats.get('severity', '-')}")
print(f"trigger_type: {stats.get('trigger_type', '-')}")
print(f"waiting_behavior: {stats.get('waiting_behavior', '-')}")
print(f"attempted: {stats.get('attempted', 0)}")
print(f"completed: {stats.get('completed', 0)}")
print(f"remaining: {stats.get('remaining', 0)}")
print(f"evidence_entries: {stats.get('evidence_total', 0)}")
print(f"errors: {len(errors)}")
for e in errors:
print(f" error: {e}")
print(f"warnings: {len(warnings)}")
for w in warnings:
print(f" warning: {w}")
verdict = "FAIL" if errors else "PASS"
print(f"verdict: {verdict}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("path", nargs="?", default=None)
parser.add_argument("--make-fixture", action="store_true")
parser.add_argument("--make-valid", action="store_true")
args = parser.parse_args()
if args.make_fixture:
packet = FIXTURE
elif args.make_valid:
packet = VALID
elif args.path:
try:
with open(args.path, "r", encoding="utf-8") as fh:
packet = json.load(fh)
except (OSError, json.JSONDecodeError) as exc:
print(f"error: cannot read packet: {exc}")
return 2
else:
print("error: give a packet path, --make-fixture, or --make-valid")
return 2
errors, warnings, stats = check_packet(packet)
print_report(errors, warnings, stats)
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())
publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.