Summary#
public class QuickEstimate : Skill<Repair, Estimate> { public override decimal? Budget => 0.012m; } // this job
public class Estimator : Agent { public override AgentLimits Limits => new AgentLimits { PerTask = 0.10m, Daily = 2m }; }
app.Agents = new AgentsSetup { Agents = [new Estimator()], TaskBudget = 0.05m }; // the rest
SetBudget("agent", "Estimator", LlmBudgetWindow.Day, 5m); // a manager's budget binds the same callsSignature#
class Skill<TIn, TOut> { virtual decimal? Budget; } // dollars for one piece of this work
class AgentLimits { decimal? PerTask; decimal? Daily; } // per piece of work, and for the agent per day
class AgentsSetup { decimal TaskBudget = 0.50m; } // every piece of work that says nothing else
AgentRef AgentRef.On(string scope, decimal? limit = null) // charge the work to something of yours, and hold it to that
AgentRef AgentRef.On(LlmLimit charge) // …with the limit said in full
// from Osysharp.Llm — the scopes an agent's calls are charged to: "agent:<name>", "skill:<name>", "task:<id>"
void SetBudget(string scope, string subject, LlmBudgetWindow window, decimal dollars)
decimal SpentOn(string scope)Description#
Which limit applies to a piece of work?#
The most specific one that is set: the skill's Budget, then the agent's Limits.PerTask, then the app's
TaskBudget. The receipt carries it as LimitMicros.
A piece of work [[agent-delegation#money|handed on]] by another is opened with the least of that and what the work above it has left, and every call it makes is held to the limit of every piece above it as well.
What happens when the money runs out?#
Each call reserves what it could cost — its input plus the most its answer may run to — before it is sent. A call
that does not fit is not sent: the work ends as Stopped, its Problem says which limit had no room, and what its
tools had already done stays done. The receipt shows what was really spent, which is never more than the limit.
What else binds an agent's calls?#
The agent's own Limits.Daily, counted across everything it does in a day, and any budget set
for the scopes its calls carry: SetBudget("agent", name, …), SetBudget("skill", name, …), a user's or a
tenant's. A call must fit all of them.
How do I charge work to something of mine?#
Agents.Of<Estimator>().On("job:" + job.Id, limit: 2.00m).Do(…) names the scope on every model call of the work, so
the cost is counted there as each call returns — not when the work reports back — and, with a limit (US
dollars), room is reserved against it before each call leaves: work that would pass it is stopped where it stands,
exactly as when its own money runs out. Without a limit the work is counted there and not held. The receipt records
it (AgentTask.ChargedTo), and SpentAgainst("job:" + job.Id) reads the figure as it stands. It binds this work
only; it is how a case, an order or an assignment holds the agent work done for it to one
budget.
Where do I read what was spent?#
On the receipt (SpentMicros), on each model call's line of the record, and on the meter: SpentOn("task:" + id),
SpentOn("agent:Estimator"), SpentOn("skill:EstimateRepair"). They agree, because they are the same rows.
Examples#
app Workshop {
use Osysharp.Agents;
use Osysharp.Agents.Testing;
use Osysharp.Llm;
use Osysharp.Identity;
use Osysharp.Observability;
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 } ],
};
app.Agents = new AgentsSetup { Agents = [new Estimator()], TaskBudget = 0.05m };
[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);
// The kit's questions, answered by this app.
policy UsesAgents => IsMechanic || IsManager; // gives agents work, reads the work they asked for
policy SupervisesAgents => IsManager; // names sponsors, pauses and stops, reads that work happened
policy AuditsAgents => IsManager; // reads everything, content included
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 = "";
/// What the customer is charged. Only a mechanic or the manager sets it.
decimal Price;
security {
allow read, update when IsMechanic || IsManager || IsHelper;
allow create when IsMechanic || IsManager;
deny update Price when IsHelper;
}
}
/// What an estimate says.
public class Estimate {
public decimal Hours;
public string Summary = "";
public bool NeedsParts;
}
/// The parts a fault needs.
[Action("Look up the parts a fault needs", Query)]
public List<string> PartsFor(string fault) { return fault.Contains("brake") ? ["brake pads", "cable"] : []; }
/// Write a note on the repair's ticket.
[Action("Write a note on the repair")]
public string NoteRepair(Repair repair, string note) { repair.Note = note; return $"Noted on {repair.Ticket}."; }
/// Set what the customer is charged.
[Action("Set the price")]
public string SetPrice(Repair repair, decimal price) { repair.Price = price; return $"{repair.Ticket} costs {price}."; }
/// Works out how long one repair takes.
public class EstimateRepair : Skill<Repair, Estimate> {
public override string Description => "Estimate one repair.";
public override string Instructions => "Look up the parts first. Write the estimate as a note on the repair.";
public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(NoteRepair), Actions.Of(SetPrice)];
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;
}
/// Estimates the repairs that come in. It holds the Helper role and nothing more.
[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(), new QuickEstimate()];
public override Permits May => Permits.ChangesAlone(); // it notes and prices by itself
}
/// The same job at a glance, on a small budget of its own.
public class QuickEstimate : Skill<Repair, Estimate> {
public override string Description => "Estimate a repair at a glance.";
public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(NoteRepair)];
public override decimal? Budget => 0.012m;
}
[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" };
new AgentSeat { Agent = "Estimator", Sponsor = noor }; // the manager answers for the agent
}
principal Mia => Person.Single(p => p.Email == "[email protected]");
principal Noor => Person.Single(p => p.Email == "[email protected]");
string RepairArgs(Repair r, string rest) { return "{\"repair\":\"" + r.Id.ToString() + "\"" + rest + "}"; }
[Test(Shop)]
[runas(Mia)]
void Work_is_stopped_when_its_money_runs_out_and_keeps_what_it_did() {
// The job may spend 12000 millionths. Each call reserves a little over 5000 before it leaves; the two calls here
// cost 4000 each, which leaves 4000 — less than a third call reserves.
var repair = Repair.Single(r => r.Ticket == "T-7");
var script = AgentScript.Of(
Says.Tool(Actions.Of(NoteRepair), RepairArgs(repair, ",\"note\":\"Looked at it.\""), 1000, 600),
Says.Tool(Actions.Of(PartsFor), "{\"fault\":\"rear brake\"}", 1000, 600),
Says.HandBack(new Estimate { Hours = 1 }));
ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
var outcome = await Agents.Of<Estimator>().Do(new QuickEstimate(), repair);
Assert.Equal(AgentTaskStatus.Stopped, outcome.Status);
Assert.StartsWith("Its money ran out", outcome.Problem);
var receipt = AgentTask.Single();
Assert.Equal(12000, receipt.LimitMicros);
Assert.Equal(8000, receipt.SpentMicros);
Assert.Equal(2, receipt.ModelCalls);
Assert.Equal("Looked at it.", Repair.Single(r => r.Ticket == "T-7").Note); // what it did stays done
}
[Test(Shop)]
void A_managers_budget_binds_the_agent_and_the_meter_agrees_with_the_receipt() {
ScriptedAnswer.Stub((model, request) => Says.HandBack(new Estimate { Hours = 1 }, 700, 60));
Guid task;
runas (Mia) {
var repair = Repair.Single(r => r.Ticket == "T-7");
task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair)).Task;
Assert.Equal(1000, AgentTask.Single().SpentMicros); // 700 + 5 × 60
}
runas (Noor) {
Assert.Equal(0.001m, SpentOn("task:" + task.ToString()));
Assert.Equal(0.001m, SpentOn("agent:Estimator"));
// A tenth of a cent a day for the agent — less than one call reserves.
SetBudget("agent", "Estimator", LlmBudgetWindow.Day, 0.001m);
UnitOfWork.Commit();
}
runas (Mia) {
var repair = Repair.Single(r => r.Ticket == "T-7");
var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
Assert.Equal(AgentTaskStatus.Stopped, outcome.Status);
Assert.Equal(0, outcome.CostMicros);
}
}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 who is in charge of it
- 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 every piece of work leaves
- Model spend — budgets, the meter and the price book (Llm kit) — budgets, the meter and prices in the model kit