# Guards and trying again — the app's own checks on an agent's work, and what happens when work fails (Agents kit)

> A guard is a class of the app's that is asked at four points of every piece of an agent's work — before a request leaves for a model, after the model answers, before a tool runs (or is proposed) and after it answers — and may stop the step with a reason, which is a line of the record. The kit ships none. A skill may say that work which fails is tried again from the start, each attempt a piece of work of its own; work whose last attempt failed raises `AgentWorkGaveUp`, once.

<!-- id: agent-guards · area: agent · stability: preview · html: https://osysharp.com/reference/agent/guards/ -->

## Summary        {#summary}
```osy syntax
public class NoCardNumbers : AgentGuard {
  public override GuardVerdict BeforeTool(GuardedWork work, ActionInfo tool, string argumentsJson) {
    return LooksLikeACard(argumentsJson) ? GuardVerdict.Block("A card number is not written anywhere by an agent.") : GuardVerdict.Allow();
  }
}
app.Agents = new AgentsSetup { Agents = [new Estimator()], Guards = [new NoCardNumbers()] };

public class EstimateRepair : Skill<Repair, Estimate> {
  public override RetryRule Retry => new RetryRule { Attempts = 2 };     // failed work is tried once more
}
```

## Signature      {#signature}
```osy syntax
class AgentGuard {
  virtual GuardVerdict BeforeModel(GuardedWork work, LlmRequest request);
  virtual GuardVerdict AfterModel(GuardedWork work, LlmResponse response);
  virtual GuardVerdict BeforeTool(GuardedWork work, ActionInfo tool, string argumentsJson);
  virtual GuardVerdict AfterTool(GuardedWork work, ActionInfo tool, string resultJson);
}
class GuardVerdict { static GuardVerdict Allow(); static GuardVerdict Block(string why); }
class GuardedWork  { string Agent; string Skill; Guid Task; Guid? Person; }
class AgentsSetup  { …; List<AgentGuard> Guards; }

class SkillBase { virtual RetryRule Retry; }   class RetryRule { int Attempts = 1; }
entity AgentTask { …; int Attempt; AgentTask? RetryOf; }
// event: AgentWorkGaveUp { Task, Attempts, Problem }
```

## Description    {#description}

### What does a guard see, and what does blocking do?   {#guards}
Each guard in `AgentsSetup.Guards` is asked in the order listed; the first that blocks decides.

| Asked | With | When it blocks |
|---|---|---|
| `BeforeModel` | the request about to leave | nothing is sent; the work is `Stopped` with the reason; it cost nothing |
| `AfterModel` | the model's answer | the answer is not acted on; the call is paid for; the work is `Stopped` |
| `BeforeTool` | the tool and its arguments — before it runs, **and before it is proposed** | the tool does not run; the model is told why and goes on |
| `AfterTool` | the tool and what it answered | the tool HAS run; what it answered is kept from the model, which is told why |

A block is a line of the [record](https://osysharp.com/reference/agent/task-log/) (`TraceKind.Guarded`) naming the guard and the reason. A guard is
ordinary code of the app's: it can read the app's data, and it runs as whoever the work runs as.

A guard is a second check, not the first. Who may do what is the app's `security {}`; what an agent does alone is
its [declaration](https://osysharp.com/reference/agent/autonomy/). A guard is for what neither can say — what text may go to a model, which
arguments are never acceptable.

A streamed answer ([[agent-ask#ask]]) has been shown as it arrived when `AfterModel` is asked: blocking there keeps
it from being acted on, not from having been seen.

### What happens when work fails?   {#retry}
Nothing more, unless the skill says: `Retry => new RetryRule { Attempts = 2 }` has `Do` try work that ended
`Failed` once more — from the beginning, as a piece of work of its own with its own receipt and budget
(`Attempt = 2`, `RetryOf` the failed one). The outcome names the last attempt. Work that was stopped, called off or
that [[agent-people#waiting|waited on a person]] is not tried again.

When the last attempt has failed, `AgentWorkGaveUp` is raised — once, where `AgentTaskFailed` is raised per attempt.
It is what a "needs attention" list is filled from; with [`Osysharp.Agents.HumanRequests`](https://osysharp.com/reference/agent/requests/) in the
app it becomes a request in the sponsor's inbox.

## Examples       {#examples}
```osy title="a guard stops a tool and keeps an answer from the model; failed work is tried again" run app=agent-guards
app Workshop {
  use Osysharp.Agents;
  use Osysharp.Agents.Testing;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Observability;
  use Osysharp.Workflow;
  model "model/**/*.osy";
}

using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Identity;
using Osysharp.Llm;

app.Llm = new LlmSetup {
  Models = [ new LlmModel { Name = "Everyday", Client = new ScriptedModel { Name = "Everyday" }, Model = "scripted-everyday",
                            Price = new LlmPrice { Input = 1, Output = 5 }, MaxOutputTokens = 1000 } ],
  Default = "Everyday",
};
app.Agents = new AgentsSetup { Agents = [new Estimator()], TaskBudget = 0.05m, Guards = [new ShopRules()] };

[Role] enum ShopRole { Authenticator, Mechanic, Manager, Helper }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsMechanic => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Mechanic);
policy IsManager  => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Manager);
policy IsHelper   => RoleGrant.Any(g => g.Member == user && g.Role == ShopRole.Helper);

policy UsesAgents        => IsMechanic || IsManager;
policy SupervisesAgents  => IsManager;
policy AuditsAgents      => IsManager;
policy ReadsModelUsage   => IsManager;
policy ManagesModelSpend => IsManager;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}

/// One bike in for repair.
entity Repair {
  [Required, Unique, MaxLength(40)] string Ticket;
  [Required, MaxLength(200)] string Fault;
  [MaxLength(400)] string Note = "";
  /// Where the customer keeps the spare key — for the mechanic, not for a model.
  [MaxLength(200)] string KeyIsKept = "";
  security { allow read, create, update when IsMechanic || IsManager || IsHelper; }
}

public class Estimate {
  public decimal Hours;
  public string Summary = "";
}

[Action("Write a note on the repair")]
public string NoteRepair(Repair repair, string note) { repair.Note = note; return $"Noted on {repair.Ticket}."; }

[Action("Read where the key is kept", Query)]
public string WhereIsTheKey(Repair repair) { return "The key is " + repair.KeyIsKept; }

/// The workshop's own rules for its agents.
public class ShopRules : AgentGuard {
  // No note an agent writes blames the customer.
  public override GuardVerdict BeforeTool(GuardedWork work, ActionInfo tool, string argumentsJson) {
    return argumentsJson.Contains("customer's fault") ? GuardVerdict.Block("A note does not blame the customer.") : GuardVerdict.Allow();
  }
  // Where a key is kept is not something a model is told.
  public override GuardVerdict AfterTool(GuardedWork work, ActionInfo tool, string resultJson) {
    return resultJson.Contains("The key is") ? GuardVerdict.Block("Where a key is kept stays in the workshop.") : GuardVerdict.Allow();
  }
}

public class EstimateRepair : Skill<Repair, Estimate> {
  public override string Description => "Estimate one repair.";
  public override List<ActionInfo> Tools => [Actions.Of(NoteRepair), Actions.Of(WhereIsTheKey)];
  public override string Brief(Repair r) => $"Ticket {r.Ticket}: {r.Fault}. Its id is {r.Id}.";
  public override string? Check(Estimate e) => e.Hours <= 0 ? "An estimate takes some time." : null;
  // Work that fails is tried once more, from the start.
  public override RetryRule Retry => new RetryRule { Attempts = 2 };
}

[Actor(Roles = [ShopRole.Helper])]
public class Estimator : Agent {
  public static Person Principal => new Person { Email = "estimator@workshop.example", DisplayName = "Estimator" };
  public override string Purpose => "You estimate bicycle repairs for a small workshop.";
  public override List<SkillBase> Skills => [new EstimateRepair()];
  public override Permits May => Permits.ChangesAlone();
}

/// What the workshop heard.
entity Heard {
  [Required, MaxLength(40)] string What;
  [MaxLength(600)] string About = "";
  security { allow read, create when IsAuthenticated; }
}
[On] void GaveUp(AgentWorkGaveUp e) { new Heard { What = "gave up", About = e.Attempts.ToString() + " attempts" }; }

[TestFixture]
void Shop() {
  var mia = new Person { Email = "mia@workshop.example", DisplayName = "Mia" };
  var noor = new Person { Email = "noor@workshop.example", DisplayName = "Noor" };
  new RoleGrant { Member = mia, Role = ShopRole.Mechanic };
  new RoleGrant { Member = noor, Role = ShopRole.Manager };
  new Repair { Ticket = "T-7", Fault = "rear brake does not bite", KeyIsKept = "under the saddle" };
  new AgentSeat { Agent = "Estimator", Sponsor = noor };
}
principal Mia => Person.Single(p => p.Email == "mia@workshop.example");

string RepairArgs(Repair r, string rest) { return "{\"repair\":\"" + r.Id.ToString() + "\"" + rest + "}"; }

[Test(Shop)]
[runas(Mia)]
void A_guard_stops_a_tool_and_keeps_an_answer_from_the_model() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  var script = AgentScript.Of(
    Says.Tool(Actions.Of(NoteRepair), RepairArgs(repair, ",\"note\":\"Worn pads: the customer's fault.\"")),
    Says.Tool(Actions.Of(WhereIsTheKey), RepairArgs(repair, "")),
    Says.Tool(Actions.Of(NoteRepair), RepairArgs(repair, ",\"note\":\"Worn pads.\"")),
    Says.HandBack(new Estimate { Hours = 1, Summary = "Pads." }));
  ScriptedAnswer.Stub((model, request) => script.Answer(model, request));

  var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);

  Assert.True(outcome.Done);
  // The first note was never written; the one that blames nobody was.
  Assert.Equal("Worn pads.", Repair.Single(r => r.Ticket == "T-7").Note);
  // Both blocks are lines of the record, each naming the guard and its reason.
  var said = Trajectory.Said(outcome.Task);
  Assert.Contains("Shop rules stopped NoteRepair: A note does not blame the customer.", said);
  Assert.Contains("Shop rules kept what WhereIsTheKey answered from the model: Where a key is kept stays in the workshop.", said);
  // The tool that ran is counted as run; the one that was stopped is not.
  Assert.Equal(["WhereIsTheKey", "NoteRepair"], Trajectory.Of(outcome.Task));
}

[Test(Shop)]
[runas(Mia)]
void Failed_work_is_tried_again_and_gives_up_once() {
  var repair = Repair.Single(r => r.Ticket == "T-7");
  // An estimate of no time at all is refused, again and again: the attempt fails. The second attempt gets it right.
  var bad = AgentScript.Of(Says.HandBack(new Estimate { Hours = 0 }), Says.HandBack(new Estimate { Hours = 0 }), Says.HandBack(new Estimate { Hours = 0 }));
  var good = AgentScript.Of(Says.HandBack(new Estimate { Hours = 1, Summary = "An hour." }));
  ScriptedAnswer.Stub((model, request) => AgentTask.Count() >= 2 ? good.Answer(model, request) : bad.Answer(model, request));

  var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);

  Assert.True(outcome.Done);
  var second = AgentTask.Single(t => t.Attempt == 2);
  Assert.Equal(outcome.Task, second.Id);
  Assert.Equal(AgentTaskStatus.Failed, second.RetryOf!.Status);
  Background.Settle();
  Assert.Equal(0, Heard.Count(h => h.What == "gave up"));

  // Work whose every attempt fails gives up once, after the last.
  ScriptedAnswer.Stub((model, request) => bad.Answer(model, request));
  Assert.Equal(AgentTaskStatus.Failed, (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair)).Status);
  Background.Settle();
  Assert.Equal("2 attempts", Heard.Single(h => h.What == "gave up").About);
}
```

## See also       {#see-also}
- [What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)](https://osysharp.com/reference/agent/autonomy/) — what an agent does alone, proposes, and never does
- [An agent asks a person, and waits — the person rung of the ladder of help (Agents kit)](https://osysharp.com/reference/agent/people/) — asking a person, and waiting
- [An agent's proposals and questions in a person's inbox, and an inbox item in an agent's hands (Agents.HumanRequests kit)](https://osysharp.com/reference/agent/requests/) — work an agent gave up on, in its sponsor's inbox
- [A skill — a typed job an agent can be given, its check and its tools (Agents kit)](https://osysharp.com/reference/agent/skills/) — a typed job, its check and its tools
- [The receipt and the record — what every piece of an agent's work leaves (Agents kit)](https://osysharp.com/reference/agent/task-log/) — the receipt and the record
