Summary#
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#
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#
What does a guard see, and what does blocking do?#
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 (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. 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?#
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 in the
app it becomes a request in the sponsor's inbox.
Examples#
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 = "[email protected]", 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 = "[email protected]", DisplayName = "Mia" };
var noor = new Person { Email = "[email protected]", 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 == "[email protected]");
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#
- What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit) — 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) — 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) — 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) — a typed job, its check and its tools
- The receipt and the record — what every piece of an agent's work leaves (Agents kit) — the receipt and the record