DEV Community

Odd_Background_328
Odd_Background_328

Posted on

Perplexity's Agent Skills Need an Undo Path Before They Need More Skills

Perplexity added "Skills" to its Agent API, letting developers compose built-in and custom skills for more complex agent outputs. More skills mean more actions, and more actions mean more ways to produce an irreversible change. Before you add a fifth skill to your agent, make sure the first four have an undo path.

The undo problem

An agent skill that writes, sends, publishes, or deploys is an irreversible action if there is no rollback. The more skills an agent has, the more irreversible actions it can take in a single session. A chain of skills (research → draft → format → publish) can complete before a human notices the first step was wrong.

The undo contract

Every agent skill should declare:

{
  "skill_name": "publish_to_blog",
  "action_type": "write",
  "reversible": true,
  "undo_method": "set_published_false",
  "undo_timeout_seconds": 3600,
  "undo_side_effects": ["SEO index will retain the URL for up to 24h"]
}
Enter fullscreen mode Exit fullscreen mode

If reversible is false, the skill should require explicit human approval before execution.

A skill registry with undo support

# skill_registry.py
"""
Registers agent skills with undo metadata.
Blocks irreversible skills from running without explicit approval.
"""
from dataclasses import dataclass
from typing import Optional, Callable
import json

@dataclass
class Skill:
    name: str
    action_type: str  # "read", "write", "send", "deploy"
    reversible: bool
    undo_fn: Optional[Callable] = None
    undo_timeout_seconds: int = 3600
    side_effects: str = ""
    requires_approval: bool = False

class SkillRegistry:
    def __init__(self):
        self.skills = {}
        self.execution_log = []

    def register(self, skill: Skill):
        # Irreversible write/send/deploy skills require approval
        if not skill.reversible and skill.action_type in ("write", "send", "deploy"):
            skill.requires_approval = True
        self.skills[skill.name] = skill

    def execute(self, skill_name: str, inputs: dict, approved: bool = False):
        skill = self.skills.get(skill_name)
        if not skill:
            raise ValueError(f"Unknown skill: {skill_name}")

        if skill.requires_approval and not approved:
            return {
                "status": "blocked",
                "reason": "Skill is irreversible and requires explicit approval",
                "skill": skill_name,
                "action_type": skill.action_type
            }

        # Execute the skill (simulated)
        result = {"status": "executed", "skill": skill_name, "inputs": inputs}
        self.execution_log.append({
            "skill": skill_name,
            "inputs": inputs,
            "result": result,
            "undo_available": skill.reversible,
            "executed_at": "2026-07-20T15:00:00Z"
        })
        return result

    def undo(self, execution_index: int):
        entry = self.execution_log[execution_index]
        skill = self.skills[entry["skill"]]

        if not skill.undo_fn:
            return {
                "status": "failed",
                "reason": "No undo function registered",
                "skill": entry["skill"]
            }

        undo_result = skill.undo_fn(entry["inputs"])
        entry["undone"] = True
        entry["undo_result"] = undo_result
        return {"status": "undone", "skill": entry["skill"], "result": undo_result}

# Example usage
if __name__ == "__main__":
    registry = SkillRegistry()

    # Reversible skill
    def undo_publish(inputs):
        return f"Set published=false for {inputs.get('post_id')}"

    registry.register(Skill(
        name="publish_post",
        action_type="write",
        reversible=True,
        undo_fn=undo_publish,
        side_effects="SEO index retains URL for 24h"
    ))

    # Irreversible skill
    registry.register(Skill(
        name="send_email",
        action_type="send",
        reversible=False,
        undo_fn=None,
        side_effects="Email cannot be un-sent"
    ))

    # Test reversible skill
    result1 = registry.execute("publish_post", {"post_id": "123", "title": "Test"})
    print(f"Publish: {result1}")

    # Undo it
    undo1 = registry.undo(0)
    print(f"Undo publish: {undo1}")

    # Test irreversible skill without approval
    result2 = registry.execute("send_email", {"to": "user@example.com"})
    print(f"Send without approval: {result2}")
    assert result2["status"] == "blocked"

    # Test with approval
    result3 = registry.execute("send_email", {"to": "user@example.com"}, approved=True)
    print(f"Send with approval: {result3}")
    assert result3["status"] == "executed"

    # Try to undo (should fail)
    undo2 = registry.undo(1)
    print(f"Undo send: {undo2}")
    assert undo2["status"] == "failed"

    print("\nPASS: undo contract enforced")
Enter fullscreen mode Exit fullscreen mode

What the registry enforces

Skill type Execution Undo
Read-only Always allowed Not needed
Reversible write Always allowed Available within timeout
Irreversible write Blocked without approval Not available

What to check

List every skill your agent currently has. For each one, fill in the undo contract above. If you have a skill where reversible is false and requires_approval is not enforced, you have an irreversible action running without a safety gate.

Top comments (0)