Summary#
policy UsesAgents => IsStaff; // gives agents work, reads the work they asked for
policy SupervisesAgents => IsLead; // …and with it: sponsors, pauses, reads every receipt, decides others' proposals
policy PausesAgents => SupervisesAgents || IsOnCall; // hand one of the four to somebody else
policy ReadsAllAgentWork => SupervisesAgents || AuditsAgents || IsFinance; // finance reads the costs, not the words
PauseAgent("Sorter", "It flagged forty crates in ten minutes."); ResumeAgent("Sorter", "The scale is fixed.");
SponsorAgent("Sorter", person); SeatChangesOf("Sorter"); // who, what, when, why
var again = RetryAgentWork(task); // the same brief, a receipt of its ownSignature#
virtual policy SponsorsAgents => SupervisesAgents; // name the person who answers for an agent
virtual policy PausesAgents => SupervisesAgents; // pause it, let it work again
virtual policy ReadsAllAgentWork => SupervisesAgents || AuditsAgents; // every receipt — not what the work said
virtual policy ReadsAgentWorkOf(string chargedTo) => false; // a receipt by what it is charged to
virtual policy DecidesAgentProposals => SupervisesAgents; // decide a proposal put to somebody else
void PauseAgent(string agent, string why = "") // [Authorize(PausesAgents)]
void ResumeAgent(string agent, string why = "") // [Authorize(PausesAgents)]
void SponsorAgent(string agent, P sponsor) // [Authorize(SponsorsAgents)]
List<AgentSeatChange> SeatChangesOf(string agent) // newest first
entity AgentSeatChange { string Agent; AgentSeatChangeKind What; P? By; DateTime At; string Why; P? Sponsor; }
enum AgentSeatChangeKind { Sponsored, Paused, Resumed }
[Event] AgentSeatChanged { AgentSeatChange Change }
Guid RetryAgentWork(AgentTask task) // [Authorize(UsesAgents)] → the new attempt's receiptDescription#
Who may do what?#
Every policy about an app's agents, with the kit that declares it. A policy admits nobody until the app answers it, unless the last column says otherwise.
| policy | kit | what it admits somebody to | until the app says otherwise |
|---|---|---|---|
UsesAgents | Agents | give agents work; read the work they asked for | nobody |
SupervisesAgents | Agents | call off any work, answer any question an agent asked, take back any standing rule, correct any work, read every piece of work | nobody |
SponsorsAgents | Agents | name the person who answers for an agent | whoever supervises |
PausesAgents | Agents | pause an agent, and let it work again | whoever supervises |
ReadsAllAgentWork | Agents | read the receipt of every piece of work: which agent, for whom, where it stands, what it cost — not what it said | whoever supervises or audits |
ReadsAgentWorkOf(chargedTo) | Agents | read the receipt — its figures, not its words — of each piece of work whose ChargedTo the app's answer admits ([[#tenants]]) | nobody |
DecidesAgentProposals | Agents | decide a proposal that was put to somebody else | whoever supervises |
WidensAgentAutonomy | Agents | say "from now on, alone" for the work an agent does for them, and take it back (What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)) | nobody |
TeachesAgents | Agents | make an instruction active, retire one, put an agent back to an earlier set (Correcting an agent, and what it is told from then on — corrections, instructions, instruction sets and rollback (Agents kit)); read every piece of work with what it said, and correct it | nobody |
DirectsAgentsForOthers | Agents | give an agent work for somebody else, which then runs with what that person may do (Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit)) | nobody |
AuditsAgents | Agents | read every piece of work with what it said | nobody |
ErasesAgentRecords | Agents | erase what a piece of work said, and whose it was (Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request) | nobody |
AgentsMayConsult | Agents | whether an agent may ask the model it consults (Asking for help — an agent that consults a more capable model (Agents kit)) | everybody |
ReadsModelUsage | Llm | read the meter: every model call with its model, tokens and cost (Model spend — budgets, the meter and the price book (Llm kit)) | nobody |
ReadsModelUsageOf(scopes) | Llm | read each model call whose Scopes the app's answer admits ([[#tenants]]) | nobody |
ManagesModelSpend | Llm | set prices and budgets | nobody |
AsksPeople | HumanRequests | put a request to a person — how a piece of work is handed to somebody | nobody |
OverseesRequests | HumanRequests | read every request an agent put to a person, call one off, give it more time | nobody |
FollowsProcesses | Processes | open the list of process runs, and a run (Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)) | nobody |
What does one line give a supervisor?#
policy SupervisesAgents => IsLead; is one line for five things: supervising itself, and the four policies that start
out as whoever supervises. An app that wants one of the four elsewhere says that one:
policy PausesAgents => SupervisesAgents || IsOnCall; // whoever is on call may pause at night
policy ReadsAllAgentWork => SupervisesAgents || AuditsAgents || IsFinance; // finance reads every receipt
policy DecidesAgentProposals => SupervisesAgents || IsDeputy; // the deputy decides for the leadEverything else is answered by name or not at all: a supervisor does not teach, widen, give work for others, read the content of a record or erase it until the app says so, a line each. The policies of the kits beside — money, requests, processes — are those kits' own, and an app answers each where it uses that kit.
What does somebody who reads every receipt see?#
The figures, and not the words. Read through ReadsAllAgentWork by somebody who neither supervises nor audits, a
receipt of somebody else's work gives the agent and the skill, who it was for, where it stands, when it started and
ended, what it spent and may spend, and its counts of calls — and reads as nothing for what it was about, what it
handed back, how it ended in words and what it waits for. The record of its steps, its proposals and its questions
are not read at all. So finance can be given what the agents cost without reading a word of the work.
Who reads what a piece of work said?#
Whoever the work was for and whoever gave it; whoever AuditsAgents admits; and whoever TeachesAgents admits —
nobody can judge whether an agent did the right thing, or correct it, without the data it acted on. A teacher reads
the receipt with its words, every line of the record with its content, the proposals and the questions, for every
agent: TeachesAgents is one answer for the whole app, so a teacher teaches — and reads — all of them. A supervisor
reads every receipt and every sentence of a record, and not the content of its lines. What was erased
(Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request) is gone from the row, for a teacher as for anybody.
In an app of many organisations?#
Every policy above is ONE answer for the whole app, which is right for a team and wrong for an app of many
organisations, shops or clients: ReadsAllAgentWork => IsAuditor would show one organisation's auditor every other's
work. Two policies are asked a row at a time instead, of what that row is charged to — a piece of work's ChargedTo
(Agents.Of<T>().On("report:" + report.Id)) and a model call's Scopes, one scope a line:
// an organisation's auditor reads the receipts and the model calls of that organisation's reports, and no others
policy ReadsAgentWorkOf(string chargedTo) => Report.Any(r => chargedTo.Contains("report:" + r.Id.ToString())
&& OrgRole.Any(g => g.Member == user && g.Role == Role.Auditor && g.Organization == r.Organization));
policy ReadsModelUsageOf(string scopes) => Report.Any(r => scopes.Contains("report:" + r.Id.ToString())
&& OrgRole.Any(g => g.Member == user && g.Role == Role.Auditor && g.Organization == r.Organization));A receipt read this way gives its figures and not its words, exactly as one read through ReadsAllAgentWork. Nobody
in any organisation needs to read the agent's own account, or the person who answers for it: an agent's account
belongs to no organisation, and the work runs — and is sponsored — whether or not whoever gives it may read either.
Who decides a proposal?#
A proposal is put to the person the work is for — to the agent's sponsor when it is for nobody — and that person
decides it (What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)). A skill may send what it proposes somewhere else: to a work queue, or to one
person it names ([[agent-autonomy#route]]); the person the work is for then reads the proposal and does not decide
it. DecidesAgentProposals says who else may, in both cases: whoever supervises, until the app names more. A yes
runs as whoever said it, and never past what they may do themselves. Whoever a proposal is put to by name reads the
work's receipt — its figures, not its words — and needs no other policy to decide.
How is an agent paused, and what is written down?#
PauseAgent(agent, why) stops an agent taking new work: whoever gives it work next is refused with AgentPaused
and the reason. Work under way finishes; calling that off is StopAgentTask. ResumeAgent(agent, why) lets it work
again. SponsorAgent(agent, person) names who answers for it — an agent with no sponsor does not run.
Each of the three leaves an AgentSeatChange — who did it, what, when and why — in the same unit of work, and raises
AgentSeatChanged. The record is written only by doing the thing, and is never changed or deleted;
SeatChangesOf(agent) reads it, newest first. Pausing what is paused, or resuming what works, writes nothing.
Can a piece of work be tried again?#
RetryAgentWork(task) starts it again from the beginning: the same agent, skill and brief, on a receipt of its own
that says which attempt it is (Attempt) and which it follows (RetryOf), for the person the first was for, charged
to what the first was charged to.
It starts from what the work was given, which is the work's content — so it is for whoever may read that: the
person the work was for, whoever gave it, and an auditor who gives agents work. Somebody who reads only that the work
happened is refused; and trying somebody else's work again needs DirectsAgentsForOthers, because it runs for that
person again.
| the work | answer |
|---|---|
| failed, was stopped or was called off | a new attempt |
| is still under way | refused: call it off first |
| finished | refused: give the agent the work again where it is given |
| was handed on by another piece of work | refused: try the work at the top again |
| was a step of a process | refused: the process was told how it ended, and has gone on |
| has been tried again already | refused: open the attempt that followed |
What will a standing rule tell me about itself?#
How often it has been used. AgentStandingRule.Used counts each time a tool ran alone because of the rule, and
LastUsedAt says when it last did — counted by the kit as the tool runs, never by hand.
Examples#
app SeedBank {
use Osysharp.Agents;
use Osysharp.Agents.Testing;
use Osysharp.Llm;
use Osysharp.Identity;
use Osysharp.Observability;
use Osysharp.Workflow;
model "model/**/*.osy";
}
using Osysharp.Identity;
using Osysharp.Llm;
using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Workflow;
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 Sampler()] };
[Role] enum VaultRole { Authenticator, Keeper, Warden, OnCall, Bookkeeper, Sampling }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsKeeper => RoleGrant.Any(g => g.Member == user && g.Role == VaultRole.Keeper);
policy IsWarden => RoleGrant.Any(g => g.Member == user && g.Role == VaultRole.Warden);
policy IsOnCall => RoleGrant.Any(g => g.Member == user && g.Role == VaultRole.OnCall);
policy IsBookkeeper => RoleGrant.Any(g => g.Member == user && g.Role == VaultRole.Bookkeeper);
policy IsSampling => RoleGrant.Any(g => g.Member == user && g.Role == VaultRole.Sampling);
// The keepers give the sampler work; the warden is in charge of it — and that one line names its sponsor, pauses
// it, reads every receipt and decides the keepers' proposals too.
policy UsesAgents => IsKeeper || IsWarden;
policy SupervisesAgents => IsWarden;
// Two of the four, handed on: whoever is on call may pause, and the bookkeeper reads every receipt.
policy PausesAgents => SupervisesAgents || IsOnCall;
policy ReadsAllAgentWork => SupervisesAgents || AuditsAgents || IsBookkeeper;
entity Person : Principal {
security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
/// A packet of seed in the vault.
entity Packet {
[Required, Unique, MaxLength(40)] string Code;
int Sprouted = 0;
bool Viable = false;
security { allow read, update when IsKeeper || IsWarden || IsSampling; allow create when IsKeeper; }
}
/// How many of a packet's twenty test seeds sprouted.
[Action("Count the sprouted seeds", Query)]
public int SproutedOf(Packet packet) { return packet.Sprouted; }
/// Mark a packet as still worth keeping.
[Action("Mark the packet viable")]
public string MarkViable(Packet packet) { packet.Viable = true; return packet.Code + " is viable."; }
public class Verdict { public bool Keep; public string Why = ""; }
public class TestPacket : Skill<Packet, Verdict> {
public override string Description => "Say whether a packet of seed is still worth keeping.";
public override List<ActionInfo> Tools => [Actions.Of(SproutedOf), Actions.Of(MarkViable)];
public override string Brief(Packet p) => $"Packet {p.Code}. Its id is {p.Id}.";
}
/// Tests the seed. It marks a packet by itself.
[Actor(Roles = [VaultRole.Sampling])]
public class Sampler : Agent {
public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Sampler" };
public override string Purpose => "You test whether stored seed still sprouts.";
public override List<SkillBase> Skills => [new TestPacket()];
public override Permits May => Permits.ChangesAlone();
}
public AgentOutcome<Verdict> Test(Packet packet) { return await Agents.Of<Sampler>().Do(new TestPacket(), packet, about: "Packet " + packet.Code); }
[TestFixture]
void Vault() {
var keeper = new Person { Email = "[email protected]", DisplayName = "Tove Lindqvist" };
var warden = new Person { Email = "[email protected]", DisplayName = "Amara Nwosu" };
var night = new Person { Email = "[email protected]", DisplayName = "Rafael Ortega" };
var books = new Person { Email = "[email protected]", DisplayName = "Hana Sato" };
new RoleGrant { Member = keeper, Role = VaultRole.Keeper };
new RoleGrant { Member = warden, Role = VaultRole.Warden };
new RoleGrant { Member = night, Role = VaultRole.OnCall };
new RoleGrant { Member = books, Role = VaultRole.Bookkeeper };
new Packet { Code = "RYE-1987", Sprouted = 17 };
new AgentSeat { Agent = "Sampler", Sponsor = warden };
}
principal Keeper => Person.Single(p => p.Email == "[email protected]");
principal Warden => Person.Single(p => p.Email == "[email protected]");
principal Night => Person.Single(p => p.Email == "[email protected]");
principal Books => Person.Single(p => p.Email == "[email protected]");
/// What the scripted sampler does with a packet: counts, marks it, and hands back.
AgentScript TheSampler() {
var packet = Packet.Single();
var id = "{\"packet\":\"" + packet.Id.ToString() + "\"}";
return AgentScript.Of(
Says.Tool(Actions.Of(SproutedOf), id, 400, 40),
Says.Tool(Actions.Of(MarkViable), id),
Says.HandBack(new Verdict { Keep = true, Why = "Seventeen of twenty sprouted." }));
}
[Test(Vault)]
void whoever_is_on_call_pauses_it_and_the_warden_lets_it_work_again() {
// A keeper gives the sampler work, and that is all.
runas (Keeper) { Assert.Denied(() => PauseAgent("Sampler", "I do not trust it.")); }
// Whoever is on call pauses it, with the reason — and may not name its sponsor.
runas (Night) {
PauseAgent("Sampler", "The growth cabinet's thermostat has failed.");
UnitOfWork.Commit();
Assert.Denied(() => SponsorAgent("Sampler", Night));
}
// Paused, it takes no new work, and whoever asks is told why.
runas (Keeper) {
var refused = Assert.Throws<AgentPaused>(() => Test(Packet.Single()));
Assert.Equal("Sampler is paused: The growth cabinet's thermostat has failed.", refused.Message);
Assert.Denied(() => ResumeAgent("Sampler"));
}
// The warden lets it work again. Both changes are on the record: who, what and why.
runas (Warden) {
ResumeAgent("Sampler", "The cabinet is repaired.");
UnitOfWork.Commit();
var changes = SeatChangesOf("Sampler");
Assert.Equal(2, changes.Count);
var paused = changes.Single(c => c.What == AgentSeatChangeKind.Paused);
Assert.Equal(Night.Id, paused.By!.Id);
Assert.Equal("The growth cabinet's thermostat has failed.", paused.Why);
Assert.Equal(Warden.Id, changes.Single(c => c.What == AgentSeatChangeKind.Resumed).By!.Id);
}
runas (Keeper) {
var sampler = TheSampler();
ScriptedAnswer.Stub((model, request) => sampler.Answer(model, request));
Assert.True(Test(Packet.Single()).Done);
}
}
[Test(Vault)]
void the_bookkeeper_reads_what_the_work_cost_and_not_what_it_said() {
runas (Keeper) {
var sampler = TheSampler();
ScriptedAnswer.Stub((model, request) => sampler.Answer(model, request));
Assert.True(Test(Packet.Single()).Done);
Assert.Equal("Packet RYE-1987", AgentTask.Single().About); // her own: all of it
}
runas (Books) {
var receipt = AgentTask.Single();
Assert.Equal("Sampler", receipt.Agent);
Assert.Equal(AgentTaskStatus.Done, receipt.Status);
Assert.Equal(Keeper.Id, receipt.Requester!.Id);
Assert.Equal(1000, receipt.SpentMicros); // 600 + 200 + 200 millionths of a dollar
Assert.Null(receipt.About); // …and none of its words
Assert.Null(receipt.Result);
Assert.Equal(0, TraceEvent.Count()); // nor the record of its steps
}
// Whoever is on call may pause the sampler, and reads nobody's work.
runas (Night) { Assert.Equal(0, AgentTask.Count()); }
runas (Warden) { Assert.Equal("Packet RYE-1987", AgentTask.Single().About); }
}
[Test(Vault)]
void work_that_failed_is_tried_again_by_whoever_it_was_for() {
Guid first = Guid.Empty;
runas (Keeper) {
// What the model hands back never fits what the skill promises, so the work fails.
var broken = AgentScript.Of(Says.HandBackJson("{\"Keep\":\"perhaps\"}"), Says.HandBackJson("{\"Keep\":\"perhaps\"}"),
Says.HandBackJson("{\"Keep\":\"perhaps\"}"), Says.HandBackJson("{\"Keep\":\"perhaps\"}"));
ScriptedAnswer.Stub((model, request) => broken.Answer(model, request));
var outcome = Test(Packet.Single());
Assert.Equal(AgentTaskStatus.Failed, outcome.Status);
first = outcome.Task;
}
// The warden reads that the work happened, and not what it was given: it is not hers to run again.
runas (Warden) {
var refused = Assert.Throws<AgentRefused>(() => RetryAgentWork(AgentTask.Single()));
Assert.StartsWith("Trying it again starts from what it was given", refused.Message);
}
runas (Keeper) {
var sampler = TheSampler();
ScriptedAnswer.Stub((model, request) => sampler.Answer(model, request));
var again = RetryAgentWork(AgentTask.Single(t => t.Id == first));
var second = AgentTask.Single(t => t.Id == again);
Assert.Equal(2, second.Attempt);
Assert.Equal(first, second.RetryOf!.Id);
Assert.Equal(AgentTaskStatus.Done, second.Status);
Assert.True(Packet.Single().Viable);
// Once: the first attempt says it has been tried again.
Assert.Throws<AgentRefused>(() => RetryAgentWork(AgentTask.Single(t => t.Id == first)));
}
}See also#
- Writing an agent — a class with a purpose, skills and an account of its own (Agents kit) — the agent class, its account and its sponsor
- The management screens — where a person runs an app's agents (Agents.Management kit) — the screens a person runs agents from, which offer each of these to whoever may
- What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit) — the gate, proposals and standing rules
- Correcting an agent, and what it is told from then on — corrections, instructions, instruction sets and rollback (Agents kit) — who makes an instruction active
- The receipt and the record — what every piece of an agent's work leaves (Agents kit) — the receipt and the record
- Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request — erasing what work said
- policy — a policy a kit asks and an app answers