Summary#
var receipt = AgentTask.Single(t => t.Id == outcome.Task); // who asked, as whom it ran, what it cost, how it ended
var steps = TraceEvent.Where(e => e.Task == receipt).OrderBy(e => e.Seq).ToList();
var check = VerifyTrace(receipt); // check.Intact, check.BrokenAt, check.ProblemSignature#
entity AgentTask {
string Agent; string Skill; string SkillVersion; AgentTaskCause Cause; string About; string Given;
int InstructionSet; string InstructionsSent; string InstructionsHash; // what people had told it — see agent-teaching
P? Requester; P Actor; // for whom, and the account it ran as
AgentTaskStatus Status; string Outcome; string? Result;
long SpentMicros; long LimitMicros; int ModelCalls; int ToolCalls; int Turns; int InputTokens; int OutputTokens;
DateTime StartedAt; DateTime? EndedAt;
}
entity TraceEvent {
AgentTask Task; int Seq; TraceKind Kind; DateTime At;
string Summary; string SummaryHash; // the step, in a sentence — `Erased.` once it is erased
string? Payload; string PayloadHash; // its content — read by fewer people than the sentence; "" once erased
DateTime? ErasedAt;
string Model; string RouteRule; int Attempts; long CostMicros; Guid? Call; // a model call
string Tool; // a tool call or its result
string PrevHash; string Hash;
}
enum TraceKind { Started, ModelCall, ToolCall, ToolResult, AskedForHelp, SentBack, Ended }
List<TraceLine> TraceOf(AgentTask task)
TraceCheck VerifyTrace(AgentTask task) // { bool Intact; int Lines; int BrokenAt; string Problem; int Erased; }
TraceCheck CheckChain(List<TraceLine> lines)
policy ErasesAgentRecords // nobody, until the app says
int EraseAgentWorkContent(AgentTask task, string why) // what it said: every line's content, help asked, the result
void AnonymiseAgentWork(AgentTask task, string why) // whose it was, what it was about, every line's sentence
void EraseAgentHelp(AgentHelp help, string why) // one request for help asked outside any piece of work
int ForgetAgentWork(AgentTask task) // the whole of a piece of work, deleted
[Event] AgentTaskStarted · AgentTaskFinished · AgentTaskFailed · AgentTaskStopped · AgentWorkErasedDescription#
What is on the receipt?#
One AgentTask per piece of work, whatever became of it: the agent, the skill and its version, what it was about,
who asked (Requester) and the account it ran as (Actor), its status and how it ended in a sentence (Outcome),
what it handed back (Result, as JSON), and the counts — model calls, tool calls, turns, tokens, and SpentMicros
against LimitMicros in millionths of a dollar. What a receipt says was spent is what the model kit's
meter says, call for call. Add your own fields with partial entity AgentTask { … }.
Work given in a workflow's run also says which: Run, RunWorkflow, RunState, RunItem, RunStarter and RunStep — the
step of the process it is (Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit)). One page lays a receipt and its record out for a person who was
not there: The inspector — one piece of an agent's work, readable by a person who was not there (Agents.Inspector kit).
A piece of work that was handed on by another has its own receipt, which names the piece above
it (Parent), the top (Root), how deep it is (Depth), its place among the pieces handed on beside it
(Sequence, from 1) and every piece above it (Lineage). The whole tree shares one record — one chain — and
WorkTree.Of(task) reads it as a tree, each piece with what it and everything under it cost, siblings in the order
they were handed on — by Sequence, so two pieces handed on in the same instant read the same way every time.
What does the record say?#
A TraceEvent per step, in order. Summary is a sentence anybody can read — "Everyday (chosen by llm:default)
asked for a tool; it cost $0.0002.", "It asked for NoteRepair (changes data); it is one of this work's tools, so it
runs as the agent, for whoever asked." — and Payload is the content: what the model was shown, what a tool was
given and what it answered. A model call's line also names the model, the router's rule, the attempts, the cost and
the id of its row on the meter.
Who reads them?#
Whoever asked for a piece of work reads its receipt and its record, content included. A person the app's
SupervisesAgents policy admits reads every receipt and every sentence, but not the content. A person
AuditsAgents admits reads everything, and so does a person TeachesAgents admits — whoever corrects an agent
reads what it acted on ([[agent-roles#words]]). ReadsAllAgentWork opens the receipts' figures and none of the
words. Nobody else reads any of it.
How do I know the record was not changed?#
Each line carries the SHA-256 of the line before it and of its own fields, and of its content's hash rather than the
content. VerifyTrace(task) walks the chain and names the first line that does not fit. Because the chain holds the
content's hash, a supervisor who cannot read the content can still verify, and content that was erased — it can be
personal data — leaves the record standing.
Can what a record says be erased?#
Yes, and the record of what happened survives it. A line's hash is made from the hash of its sentence and the hash of its content, never from the words — so the words can go and the chain still checks.
EraseAgentWorkContent(task, why)empties the content of every line (what the agent was shown, what each tool was given and answered, what the model said), what was asked and answered when it asked for help, and the result on the receipt — for the whole of the work the task is part of. The sentences, models, tools and costs stay.AnonymiseAgentWork(task, why)takes the person off it:Requesterand whoever gave the work become nobody, what it was about and how it ended in words are blanked, and every line's sentence becomesErased.with the time. What stays is that the work happened: which agent and skill, when, each step's kind, model, tool and cost.ForgetAgentWork(task)deletes the work whole — receipts, lines, requests for help — when its time is up.
VerifyTrace answers Intact after either erasure and says how many lines were erased (Erased). A line that was
changed afterwards is still found: a cost or a tool that differs breaks the line's hash; a sentence written into an
erased line is refused ("Line 4 was erased, and something was written in its place."); so is a line marked erased
that still says something. All three are refused while any part of the work is still running.
Who may: a person the app's ErasesAgentRecords policy admits — nobody, until the app says — and they read receipts
and sentences but never content. Whoever asked for the work, a supervisor and an auditor may not. An app that uses
Osysharp.Privacy writes none of this: Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request erases for a person who asks, and when a period ends.
Can my app write or correct them?#
No. Receipts and record lines are written only by the kit's own functions; app code that creates, changes or
deletes one is denied. Hear what happens instead: AgentTaskStarted, AgentTaskFinished, AgentTaskFailed and
AgentTaskStopped are events an app handles with [[function-on|[On]]].
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
}
/// What the app is not able to do.
public void RewriteReceipt(AgentTask task) { task.SpentMicros = 0; }
[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 + "}"; }
/// What the model is scripted to answer for one estimate: two tools, then the result.
AgentScript EstimateScript(Repair repair) {
return AgentScript.Of(
Says.Tool(Actions.Of(PartsFor), "{\"fault\":\"rear brake does not bite\"}"),
Says.Tool(Actions.Of(NoteRepair), RepairArgs(repair, ",\"note\":\"New pads and a cable: one hour.\"")),
Says.HandBack(new Estimate { Hours = 1, Summary = "New pads and a cable.", NeedsParts = true }));
}
[Test(Shop)]
[runas(Mia)]
void The_receipt_says_who_asked_as_whom_it_ran_and_what_it_cost() {
var repair = Repair.Single(r => r.Ticket == "T-7");
var script = EstimateScript(repair);
ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
var task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair, about: "Ticket T-7")).Task;
var receipt = AgentTask.Single(t => t.Id == task);
Assert.Equal("Estimator", receipt.Agent);
Assert.Equal("EstimateRepair", receipt.Skill);
Assert.Equal(AgentTaskStatus.Done, receipt.Status);
Assert.Equal(Mia.Id, receipt.Requester!.Id);
Assert.Equal(Actor.Of<Estimator>().Id, receipt.Actor.Id);
Assert.Equal(600, receipt.SpentMicros);
Assert.Equal(50000, receipt.LimitMicros); // the app's TaskBudget: $0.05
Assert.Equal(3, receipt.ModelCalls);
Assert.Equal(2, receipt.ToolCalls);
}
[Test(Shop)]
[runas(Mia)]
void The_record_says_every_step_in_order_and_verifies() {
var repair = Repair.Single(r => r.Ticket == "T-7");
var script = EstimateScript(repair);
ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
var task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair, about: "Ticket T-7")).Task;
Assert.Equal(["Started", "ModelCall", "ToolCall", "ToolResult", "ModelCall", "ToolCall", "ToolResult", "ModelCall", "Ended"],
Trajectory.Kinds(task));
Assert.Contains("It asked for NoteRepair (changes data); it is one of this work's tools, so it runs as the agent, for whoever asked.",
Trajectory.Said(task));
var receipt = AgentTask.Single(t => t.Id == task);
Assert.True(VerifyTrace(receipt).Intact);
// A sentence rewritten afterwards is found, and which line it was.
var lines = TraceOf(receipt);
lines[3].Summary = "PartsFor was never asked for.";
var check = CheckChain(lines);
Assert.False(check.Intact);
Assert.Equal(4, check.BrokenAt);
}
[Test(Shop)]
void Only_the_kit_writes_a_receipt_and_the_content_is_read_by_fewer() {
Guid task;
runas (Mia) {
var repair = Repair.Single(r => r.Ticket == "T-7");
var script = EstimateScript(repair);
ScriptedAnswer.Stub((model, request) => script.Answer(model, request));
task = (await Agents.Of<Estimator>().Do(new EstimateRepair(), repair, about: "Ticket T-7")).Task;
var receipt = AgentTask.Single(t => t.Id == task);
Assert.Denied(() => { RewriteReceipt(receipt); UnitOfWork.Commit(); });
UnitOfWork.Discard();
// Whoever asked reads the content.
Assert.NotNull(TraceEvent.First(e => e.Task.Id == task && e.Kind == TraceKind.ToolCall).Payload);
}
runas (Noor) {
// The manager supervises and audits here, so she reads it too.
Assert.Equal(9, TraceEvent.Count(e => e.Task.Id == task));
}
}See also#
- An agent asks a person, and waits — the person rung of the ladder of help (Agents kit) — a receipt that is
Waiting, and the record's lines for a wait and a person's decision - Guards and trying again — the app's own checks on an agent's work, and what happens when work fails (Agents kit) — a guard's block as a line of the record; an attempt and the one it follows
- 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 - What an agent may spend — a hard limit on every piece of work (Agents kit) — what a piece of work may spend
- Testing an agent without a model — Says, AgentScript and Trajectory (Agents.Testing kit) — scripting the model in a test
- Model spend — budgets, the meter and the price book (Llm kit) — the meter a receipt's cost is read from
- Privacy for the agent family — agents, the model meter, assignments and work repositories under a person's request — the agent family under a privacy request: copies, erasure and how long things are kept