Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

Who is in charge of an app's agents — every policy in one table, pausing, sponsoring, and trying work again

policy UsesAgents · SupervisesAgents · SponsorsAgents · PausesAgents · ReadsAllAgentWork · ReadsAgentWorkOf(chargedTo) · DecidesAgentProposals · WidensAgentAutonomy · TeachesAgents · DirectsAgentsForOthers · AuditsAgents · ErasesAgentRecords · PauseAgent(agent, why) · ResumeAgent(agent, why) · SponsorAgent(agent, person) · SeatChangesOf(agent) · RetryAgentWork(task)

One table of who may do what with an app's agents. An app answers UsesAgents and SupervisesAgents; supervising brings four things with it — naming an agent's sponsor, pausing it, reading the receipt of every piece of work and deciding a proposal that was put to somebody else — and each of those four is a policy of its own, an app's to hand to somebody else in one line. Widening what an agent does alone, teaching it, giving it work for somebody else, reading what its work said and erasing it are never implied. A receipt read through ReadsAllAgentWork alone shows what work was done and what it cost, and not a word of it. Pausing, resuming and sponsoring are each written down with who did it and why. RetryAgentWork starts a piece of work again from what it was given, for whoever may read that.

preview1 example compiled by CIagentskitsecurity

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 own

Signature#

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 receipt

Description#

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.

policykitwhat it admits somebody tountil the app says otherwise
UsesAgentsAgentsgive agents work; read the work they asked fornobody
SupervisesAgentsAgentscall off any work, answer any question an agent asked, take back any standing rule, correct any work, read every piece of worknobody
SponsorsAgentsAgentsname the person who answers for an agentwhoever supervises
PausesAgentsAgentspause an agent, and let it work againwhoever supervises
ReadsAllAgentWorkAgentsread the receipt of every piece of work: which agent, for whom, where it stands, what it cost — not what it saidwhoever supervises or audits
ReadsAgentWorkOf(chargedTo)Agentsread the receipt — its figures, not its words — of each piece of work whose ChargedTo the app's answer admits ([[#tenants]])nobody
DecidesAgentProposalsAgentsdecide a proposal that was put to somebody elsewhoever supervises
WidensAgentAutonomyAgentssay "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
TeachesAgentsAgentsmake 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 itnobody
DirectsAgentsForOthersAgentsgive 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
AuditsAgentsAgentsread every piece of work with what it saidnobody
ErasesAgentRecordsAgentserase 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
AgentsMayConsultAgentswhether an agent may ask the model it consults (Asking for help — an agent that consults a more capable model (Agents kit))everybody
ReadsModelUsageLlmread 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)Llmread each model call whose Scopes the app's answer admits ([[#tenants]])nobody
ManagesModelSpendLlmset prices and budgetsnobody
AsksPeopleHumanRequestsput a request to a person — how a piece of work is handed to somebodynobody
OverseesRequestsHumanRequestsread every request an agent put to a person, call one off, give it more timenobody
FollowsProcessesProcessesopen 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 lead

Everything 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 workanswer
failed, was stopped or was called offa new attempt
is still under wayrefused: call it off first
finishedrefused: give the agent the work again where it is given
was handed on by another piece of workrefused: try the work at the top again
was a step of a processrefused: the process was told how it ended, and has gone on
has been tried again alreadyrefused: 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#

Related

Writing an agent — a class with a purpose, skills and an account of its own (Agents kit)

An agent is a class of the `Osysharp.Agents` kit: what it is for, the skills it holds, what it may spend. It runs as an…

What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)

An agent's class says what it may do alone, what it must propose to a person first, and what it never does — by what a…

Correcting an agent, and what it is told from then on — corrections, instructions, instruction sets and rollback (Agents kit)

An agent gets better through people. Somebody who sees a piece of its work corrects it — what was wrong, what right…

The management screens — where a person runs an app's agents (Agents.Management kit)

Osysharp.Agents.Management draws the place a person runs an app's agents from, eight screens under one path. Agents:…

The receipt and the record — what every piece of an agent's work leaves (Agents kit)

Every piece of work leaves one receipt, `AgentTask` — who asked, the account it ran as, what it cost against its limit…

What an agent may spend — a hard limit on every piece of work (Agents kit)

Every piece of work has a limit in dollars — the skill's `Budget`, else the agent's `Limits.PerTask`, else the app's…

Model spend — budgets, the meter and the price book (Llm kit)

The Llm kit keeps three things about what model calls cost. The METER: one `ModelCall` row for every attempt, with the…

An agent's proposals and questions in a person's inbox, and an inbox item in an agent's hands (Agents.HumanRequests kit)

With the Agents kit and the HumanRequests kit both in an app, this bridge is there: what an agent proposes is an…

Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)

An agent's work is a step of a business process by being called from the process's workflow: an awaited call in a…

policy

Names an authorization rule once so every place that needs it can say the name. A policy is a boolean about the caller…

Agents (a class with a job, its own account, a budget and a record)

An agent is a class of the `Osysharp.Agents` kit: a purpose, the skills it holds and what it may spend. A skill is a…