Summary#
use Osysharp.Agents.Inspector;
app.AgentInspector = new AgentInspectorSetup { WorkPage = "/work/{task}/{step}" };
[Route("/work/{task}/{step}")] [Authorize(UsesAgents)]
component WorkPage(Guid task, int step) { render { WorkInspector(task, step: step); } }Signature#
component WorkInspector(Guid task, int step = 0, bool embedded = false)
component InspectedStepLine(InspectedStep step, bool marked = false, bool showWho = false)
component InspectedProposalCard(InspectedProposal p) · component InspectedHelpCard(InspectedHelp h)
[Slot] component AgentWorkActions(Guid task) // what can be done about it, at the head of the page
[Slot] component AgentWorkPanel(Guid task) // more about the work, under its record
[Slot] component AgentWorkOrigin(Guid task) // where the work came from, beside who asked
InspectAgentWork(Guid task) → WorkInspection? // null: not whoever-is-looking's to read
class AgentInspectorSetup { string WorkPage; } // app.AgentInspector = …
WorkPageOf(Guid task, int step = 0) → string // the work's address on the app's own page; "" when it has none
ArgumentsAsRead(agent, skill, action, argumentsJson) → List<ProposalLine> // Osysharp.Agents: a call's arguments, namedDescription#
What does the page show, and in what order?#
The order a person asks in, when they were not there:
| The work | the agent and the skill, what the work is about, how it ended and why — or what it is waiting on, and for how long so far; and anything that has been erased of it |
| What was asked | who gave the work, who it was for, as which account it ran, the process it was a step of (Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)), and what it was given, as the model was shown it |
| What it handed back | the result |
| In figures | when it started, how long it took, what it spent on models against what it may, its calls and tokens, and what else it was charged to |
| People and help | each proposal — the call, its arguments as a person reads them, why, who it was put to, who decided, after how long, their reason, what came of it (What an agent does alone, what it proposes, what it never does — the gate and proposals (Agents kit)); each request for help — of whom, the question, the answer, how long it took or what it cost (An agent asks a person, and waits — the person rung of the ladder of help (Agents kit)) |
| The work it is part of | when work was handed on (An agent hands work on to other agents — at once, typed, and never with more than it holds (Agents kit)): the tree, each piece with how it ended and what it cost — and the steps of every piece, in the one record they share |
| Steps | every line of the record, in order (The receipt and the record — what every piece of an agent's work leaves (Agents kit)), narrowed to Model, Tools, People or Problems; a wait says how long it lasted; More opens what the step said |
| What it had been told, and what it may do | the instruction set it ran with and what has been made active since that it had not been sent (Correcting an agent, and what it is told from then on — corrections, instructions, instruction sets and rollback (Agents kit)); each tool with whether the agent runs it alone, proposes it, or never does |
| The record | whether its chain still checks: no line changed, removed or slipped in |
What does a step say when it is opened?#
A model call: the model, the rule of the router that chose it, how many were tried, and the words the model
said. A tool call: what the tool is called, and its arguments each with its name and its value as the skill
names it (SkillBase.Names — a supplier's name, not its id). A tool result, a decision and the end:
what was answered. The step's number is its link: with {step} in WorkPage, /work/<task>/7 opens the page with
step 7 marked.
Who may open it, and what do they see?#
It changes nothing and widens nothing: InspectAgentWork reads the receipt, the record, the proposals and the
requests for help as whoever is looking, under the rules Osysharp.Agents declares.
| the reader | sees |
|---|---|
| whoever the work was for, and whoever gave it | all of it, with what each step said |
| whoever supervises the agents | that each step happened, what it cost and how it ended — a step's More says that what it said is kept from them |
| whoever audits the agents | all of it |
| anybody else | This piece of work is not yours to see |
What does erased work look like?#
Like work that was erased. The page says when what it said was erased and why, a step whose content is gone carries Erased, and a step whose sentence is gone says so in its place — never an empty line, and never as if nothing had been there. What happened, in what order, by which model and at what cost is unchanged, and the record still checks (Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request).
How do other kits add to it?#
Three slots ([Slot] and contributions — a place on a page that other kits, and the app, add to). AgentWorkActions(task) is at the head of the page, straight under what the
work is and how it stands — where Osysharp.Agents.Management puts calling it off, trying it again and handing it to
a person, so nobody scrolls past a long record to stop it. AgentWorkPanel(task) is under the record: "Correct this" from
Osysharp.Agents.Teaching goes there in three lines (Correct this, and an agent's Instructions page — the screens people teach an agent on (Agents.Teaching kit)):
[Contribution("Teaching")]
component TeachHere(Guid task) : AgentWorkPanel { render { WorkTeaching(task, beside: true); } }AgentWorkOrigin(task) is beside who asked: Osysharp.Agents.Processes fills it with the run of the process the
work is a step of, and the way to it. embedded: true leaves the headline out, for the work shown inside another
page — which is how a run's page opens a step in place.
Examples#
app Kennel {
use Osysharp.Agents;
use Osysharp.Agents.Testing;
use Osysharp.Agents.Inspector;
use Osysharp.Llm;
use Osysharp.Identity;
use Osysharp.Observability;
use Osysharp.Workflow;
use Osysharp.Ui;
model "model/**/*.osy";
}
using Osysharp.Identity;
using Osysharp.Llm;
using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Agents.Inspector;
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 Feeder()] };
// Where a piece of work is shown: each step's number links to its own address.
app.AgentInspector = new AgentInspectorSetup { WorkPage = "/work/{task}/{step}" };
[Role] enum KennelRole { Authenticator, Keeper, Feeding }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsKeeper => RoleGrant.Any(g => g.Member == user && g.Role == KennelRole.Keeper);
policy IsFeeding => RoleGrant.Any(g => g.Member == user && g.Role == KennelRole.Feeding);
policy UsesAgents => IsKeeper;
policy SupervisesAgents => IsKeeper;
entity Person : Principal {
security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
entity Dog {
[Required, Unique, MaxLength(60)] string Name;
decimal Kilos = 0;
[MaxLength(80)] string Ration = "";
security { allow read, update when IsKeeper || IsFeeding; allow create when IsKeeper; }
}
/// What a dog weighs.
[Action("Weigh the dog", Query)]
public decimal WeightOf(Dog dog) { return dog.Kilos; }
/// Change a dog's ration.
[Action("Set the ration")]
public string SetRation(Dog dog, string ration) { dog.Ration = ration; return dog.Name + " gets " + ration + "."; }
public class Plan { public string Ration = ""; }
public class PlanRation : Skill<Dog, Plan> {
public override string Description => "Work out one dog's daily ration.";
public override List<ActionInfo> Tools => [Actions.Of(WeightOf), Actions.Of(SetRation)];
public override string Brief(Dog d) => $"{d.Name}. Its id is {d.Id}.";
// A person reads the dog's name, not its id.
public override string Names(string action, string argument, string value) {
if (argument != "dog") { return value; }
var d = Dog.FirstOrDefault(x => x.Id == Guid.Parse(value));
return d == null ? value : d!.Name;
}
}
/// Plans the rations. It weighs by itself; changing a ration is proposed to a person.
[Actor(Roles = [KennelRole.Feeding])]
public class Feeder : Agent {
public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Feeder" };
public override string Purpose => "You plan the daily rations of a small kennel.";
public override List<SkillBase> Skills => [new PlanRation()];
}
public AgentOutcome<Plan> PlanFor(Dog dog) { return await Agents.Of<Feeder>().Do(new PlanRation(), dog, about: "Ration for " + dog.Name); }
/// One piece of the feeder's work, readable by somebody who was not there; `step` marks the step a link named.
[Route("/work/{task}/{step}")] [Authorize(UsesAgents)] [Render(CSR)] [Title("The feeder's work")]
component WorkPage(Guid task, int step) {
render { Stack(p: 5, maxW: "920px") { WorkInspector(task, step: step); } }
}
[TestFixture]
void Yard() {
var keeper = new Person { Email = "[email protected]", DisplayName = "Runa Halvorsen" };
new RoleGrant { Member = keeper, Role = KennelRole.Keeper };
new Dog { Name = "Biscuit", Kilos = 31 };
new AgentSeat { Agent = "Feeder", Sponsor = keeper };
}
principal Keeper => Person.Single(p => p.Email == "[email protected]");
[Test(Yard)]
void a_piece_of_work_is_read_back_by_somebody_who_was_not_there() {
TestClock.Set(new DateTime(2031, 2, 3, 7, 0, 0, DateTimeKind.Utc));
Guid task = Guid.Empty;
runas (Keeper) {
var dog = Dog.Single();
var id = dog.Id.ToString();
var script = AgentScript.Of(
Says.Tool(Actions.Of(WeightOf), "{\"dog\":\"" + id + "\"}", 300, 20),
Says.Tool("At 31 kg it needs more than a standard bowl.", Actions.Of(SetRation), "{\"dog\":\"" + id + "\",\"ration\":\"450 g, twice a day\"}", 400, 40),
Says.HandBack(new Plan { Ration = "450 g, twice a day" }, 500, 20));
ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
var outcome = PlanFor(dog);
Assert.True(outcome.Waiting); // changing a ration is proposed first
task = outcome.Task;
}
TestClock.Advance(TimeSpan.FromHours(5));
runas (Keeper) {
ApproveAgentProposal(ProposalsForMe().Single());
Workflow.Settle(AgentWait.Single());
}
runas (Keeper) {
var seen = InspectAgentWork(task)!;
// What it was, who asked and as whom it ran.
Assert.Equal("Ration for Biscuit", seen.About);
Assert.Equal(AgentTaskStatus.Done, seen.Status);
Assert.Equal("Given by Runa Halvorsen.", seen.GivenBy);
Assert.Equal("Done for Runa Halvorsen: never past what they may do themselves.", seen.For);
Assert.Equal("It ran as Feeder, Feeder's own account.", seen.RanAs);
Assert.Equal("450 g, twice a day", seen.Result);
// What it may do with each tool.
Assert.Equal(["Weigh the dog: Alone", "Set the ration: Proposes first"], seen.Tools.Select(t => t.Label + ": " + t.Leave.Label).ToList());
// What it proposed, and who decided after how long.
var proposed = seen.Proposals.Single();
Assert.Equal(["dog: Biscuit", "ration: 450 g, twice a day"], proposed.Shown.Select(l => l.Name + ": " + l.Value).ToList());
Assert.Equal("Approved by Runa Halvorsen on 3 Feb 2031, 12:00 after 5 hours, and it ran as them.", proposed.Decided);
// Every step in order; a tool's arguments as a person reads them; the wait and how long it lasted.
Assert.Equal(["Started", "ModelCall", "ToolCall", "ToolResult", "ModelCall", "ToolCall", "Waiting", "Decided", "ModelCall", "Ended"],
seen.Steps.Select(s => s.Kind.Name).ToList());
Assert.Equal("dog: Biscuit", seen.Steps[2].Given.Select(g => g.Name + ": " + g.Value).Single());
Assert.Equal("31", seen.Steps[3].Content);
Assert.Equal("5 hours", seen.Steps.Single(s => s.Kind == TraceKind.Waiting).Took);
Assert.Equal("/work/" + task.ToString() + "/3", seen.Steps[2].Href);
// What it cost against what it may, and whether its record still checks.
Assert.Equal("$0.0016", seen.Spent);
Assert.Equal("$0.50", seen.Limit);
Assert.True(seen.Intact);
Assert.Equal("All 10 lines of its record are as they were written.", seen.Record);
}
}Notes#
- A record that does not check says so, with the first line that does not fit; the page still shows everything it can read.
- A wait's length is the time until the next line of the record; the last line of work that is still waiting says how long it has waited so far.
See also#
- The receipt and the record — what every piece of an agent's work leaves (Agents kit) — the receipt and the record the page reads
- Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit) — the work as a step of a process, and its run
- Correct this, and an agent's Instructions page — the screens people teach an agent on (Agents.Teaching kit) — "Correct this", which goes under the record