Summary#
public class EstimateRepair : Skill<Repair, Estimate> {
public override string Description => "Estimate one repair.";
public override string Instructions => "Look up the parts first.";
public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(NoteRepair)];
public override string? Check(Estimate e) => e.Hours <= 0 ? "An estimate takes some time." : null;
}Signature#
public abstract class Skill<TIn, TOut> : SkillBase {
virtual string Description; // what the job is — the model is told, and a catalogue shows it
virtual string Instructions; // how to do it
virtual List<ActionInfo> Tools; // the app's [Action] functions it may use
virtual string Brief(TIn input); // what the model is shown of the input
virtual string? Check(TOut result); // null when it fits; otherwise the reason, sent back to the model
virtual decimal? Budget; // this job's own limit, in dollars
virtual ModelNeeds Needs; // what the app's model router decides from
virtual StopWhen Stop; // MaxTurns, SameCall, ClearResultsPast
virtual string Version; // written on every receipt
}
List<SkillInfo> Skills.Catalog() // every skill of every listed agent
List<SkillInfo> Skills.Of(string agent)
SkillInfo? Skills.Find(string agent, string skill)Description#
What are TIn and TOut?#
TIn is what the job is given — a row, a class or a string. TOut is what it hands back: a class, whose
schema the model is held to, or string for a job that answers in words. With a class result the
model finishes by calling a tool the kit adds, hand_back, whose arguments are that schema.
What does Check do that the schema does not?#
The schema says a score is a number; Check says it is from 0 to 10. A result that is not the shape, or that Check
refuses, goes back to the model with the reason and it tries again. After StuckAfter failures in a row the agent
asks for help once if it has somebody to ask, and then the work ends as Failed.
What is a tool?#
One of your app's [[function-action|[Action]]] functions, named with Actions.Of(F). Nothing is described twice:
the tool's name, its description and the schema of its arguments are the function's own, and what it reads, changes
and sends is what the compiler proved. A row argument is passed by its id and looked up with the rights the tool
runs under. A tool the skill does not list is refused whatever the model calls it, and the refusal is on the record.
How does a job stop?#
Stop bounds it: MaxTurns model calls, SameCall identical tool calls in a row (it is going in circles), and
ClearResultsPast, past which the results of old tool calls are replaced by one line each in what the model is
sent — the record still holds each one whole.
What does the catalogue hold?#
Skills.Catalog() answers a SkillInfo per agent and skill: Agent, Skill, Label, Description, Version,
Takes and HandsBack with their schemas, and Tools — each with its Action, Label, Description,
ParametersSchema and whether it ReadsOnly, Changes or Sends. It is what a picker or a canvas lists.
How does a person run a skill on demand?#
RunSkill(agent, skill, inputJson) gives the agent that job, with what the skill takes as JSON, for whoever is
signed in — exactly as Agents.Of<T>().Do(…) would: the input is read as the agent and never past what that person
may read, its proposals come to them, and it answers the receipt's id. It is for whoever the app lets give agents
work (UsesAgents).
InputFieldsOf(skill) is what a form asks for, built from the same schema another agent is handed the work by
(HandedSchema): one field per member of a class, the id of a row the skill takes by HandOn.ById, or one line for
a skill that takes text — each with its label, whether it is a number or a yes or no, and whether it is required.
InputJsonOf(fields, values) turns what was typed into the JSON the skill takes, and says what is missing or not a
number. The agent's page in Agents.Management has a Run button on each skill built from them.
var fields = InputFieldsOf(new GradeCrate()); // [ id: "The crate to grade." ]
var task = RunSkill("Packhouse.Inspector", "Packhouse.GradeCrate",
InputJsonOf(fields, new Dictionary<string, string> { ["id"] = crate.Id.ToString() }));A skill that takes a row it does not say how to find (no HandedSchema / Handed) cannot be run this way, and says
so.
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()];
public override Permits May => Permits.ChangesAlone(); // it notes and prices by itself
}
/// Not one of the skill's tools.
[Action("Scrap the bike")]
public string ScrapBike(Repair repair) { repair.Delete(); return "Scrapped."; }
[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 A_result_the_check_refuses_is_sent_back_with_the_reason() {
var repair = Repair.Single(r => r.Ticket == "T-7");
var script = AgentScript.Of(
Says.HandBack(new Estimate { Hours = 0, Summary = "Quick." }), // refused: "An estimate takes some time."
Says.HandBack(new Estimate { Hours = 1, Summary = "New pads." }));
ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
Assert.True(outcome.Done);
Assert.Equal(1, outcome.Result.Hours);
Assert.Contains("SentBack", Trajectory.Kinds(outcome.Task));
}
[Test(Shop)]
[runas(Mia)]
void A_tool_the_skill_does_not_list_never_runs() {
var repair = Repair.Single(r => r.Ticket == "T-7");
var script = AgentScript.Of(
Says.Tool(Actions.Of(ScrapBike), RepairArgs(repair, "")),
Says.HandBack(new Estimate { Hours = 1, Summary = "Still here." }));
ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
Assert.True(outcome.Done);
Assert.Equal(1, Repair.Count(r => r.Ticket == "T-7"));
Assert.False(Trajectory.Called(outcome.Task, Actions.Of(ScrapBike)));
}
[Test(Shop)]
[runas(Mia)]
void The_catalogue_describes_a_skill_from_its_own_declaration() {
var skill = Skills.Find("Estimator", "EstimateRepair")!;
Assert.Equal("Estimate one repair.", skill.Description);
Assert.Equal("Repair", skill.Takes);
Assert.Contains("\"Hours\":{\"type\":\"number\"", skill.HandsBackSchema);
Assert.Equal(["PartsFor", "NoteRepair", "SetPrice"], skill.Tools.Select(t => t.Action).ToList());
Assert.True(skill.Tools[0].ReadsOnly);
Assert.True(skill.Tools[1].Changes);
}See also#
- What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit) — a skill's
May,VersionOfandNames: what its work proposes, and what a yes covers - Guards and trying again — the app's own checks on an agent's work, and what happens when work fails (Agents kit) —
Retry: work that fails is tried again - 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
- Giving an agent work — Do, Ask, Stream, For and Decide (Agents kit) — giving an agent work:
Do,Ask,Stream,For,Decide - 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
- Testing an agent without a model — Says, AgentScript and Trajectory (Agents.Testing kit) — scripting the model in a test
- [Action] — what an app offers to be done, by name —
[Action]: the functions a skill's tools are