Summary#
task.Calls tells you what a task cost. This tells you what it contained.
foreach (var turn in task.Transcript) {
Log.Information("turn {N}: {Response}", turn.Turn, turn.Response);
}⛔ This is the one agent surface with a gate of its own, and it is worth knowing why before you use it. An agent usually runs with more authority than the person reading the screen — an auditing agent may read every line of every report, while the employee looking at the task may read only their own. Everything the agent read is in its prompt. So content does not ride the task's read rule; the agent says who may see it, and says it by default to nobody.
Signature#
task.Transcript // → List<AgentLlmTurn>AgentLlmTurn | ||
|---|---|---|
Turn | which turn of the run this was (1-based) — pairs with AgentLlmCall.Turn | |
Model | the model that answered | |
SystemPrompt | the instructions as the model received them, after every interpolation | |
Messages | the messages sent, as JSON text | |
Response | what came back — text and tool requests, in the order the model produced them, as JSON text | |
At | when the request went out | |
ContentWithheld | the app chose not to record this turn's content — see [[agent-task-transcript#withheld | when the content was never recorded]] |
Tools | what this turn invoked, in order. Empty on a turn that only talked |
AgentToolCall | |
|---|---|
Name | the tool as the model named it |
Input | the arguments the model generated, as JSON text |
Output | what the tool returned to the model, as JSON text |
IsError | the tool failed |
DurationMs | wall-clock time for the tool itself |
Sequence | order within the turn (1-based) |
Description#
Who can read it — you must say, or nobody can#
Say it on the agent, in its security { } block:
agent Auditor {
Prompt = "You review expense reports…";
Roles = [Role.Finance]; // what makes its prompts finance-scoped
security { allow read Transcript when IsFinance; } // who may read what it saw
}Omit the rule and the transcript is unreadable — by everyone, including the roles the agent itself holds. That is deliberate. A gate that defaulted to "readable" would make forgetting the rule indistinguishable from deciding you did not need one, on the one surface where those must not look alike.
The rule is written on the agent rather than on AgentTask because the exposure belongs to the
agent: Roles = [Role.Finance] is the line that puts finance-scoped data into its prompts, and the gate sits three
lines below it. Two agents in the same app can hold very different authority, and a single app-wide switch would
have to be as strict as the most privileged one — or leak through the loosest.
⚠ Its predicate is about the caller, so it takes when, not where. Name a policy (when IsFinance) or write
the condition inline; there is no row to filter.
Does it include child tasks' turns?#
Transcript includes the turns of everything the task set off, not just its own — because a task driven by a
loop makes no model call itself, so "its own" would be empty for exactly the tasks you most want to
inspect. This is the same reason [[agent-task-calls#allcalls|AllCalls]] exists, which is why there is no
AllTranscript to choose between.
⛔ If any agent involved refuses you, you get nothing at all — not the turns you would have been allowed. A transcript with some turns quietly missing cannot be told apart from an agent that simply said little, and a reader would have no way to know they were looking at a partial record. Where a job spans agents with different gates, that means you need clearance from each.
When the content was never recorded#
A turn whose ContentWithheld is true happened and cost what its call says it cost, but its
prompt and response were never written down. Three things cause it:
- the agent declares
Logging = MetadataOnly; - the app turned the call record off entirely;
- the platform dropped the body because it carried data at a redacted classification.
Show it. A blank prompt with no explanation reads as a broken screen:
foreach (var turn in task.Transcript) {
if (turn.ContentWithheld) { /* "not recorded" */ }
else { /* turn.SystemPrompt, turn.Messages, turn.Response */ }
}Examples#
A review screen showing what the agent was told and what it did about it:
using Osyrin.Agents;
[Role] enum Role { Finance, Staff }
[Principal] entity User {
[Required, MaxLength(255)] string Email;
security { allow read when IsAuthenticated; }
}
entity RoleGrant {
[Required] User User;
[Required] Role Role;
security { allow read when IsAuthenticated; }
}
policy IsFinance => RoleGrant.Any(g => g.User == user && g.Role == Role.Finance);
/// Anyone signed in may see that the review happened, and what it cost.
entity ReviewTask : AgentTask {
security { allow read when IsAuthenticated; }
}
agent Auditor {
Purpose = "Review a submitted expense report.";
Prompt = "You review expense reports against the travel policy.";
Principal = new User { Email = "auditor@ledger.demo" };
Roles = [Role.Finance];
/// …but only finance sees what it read to do so.
security { allow read Transcript when IsFinance; }
}
/// What the agent was told, and which tools it reached for.
string WhatItSaw(Guid taskId) {
var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
if (task == null) { return "no such task"; }
var turns = task.Transcript;
if (turns.Count == 0) { return "not available to you"; }
var tools = 0;
foreach (var turn in turns) { tools = tools + turn.Tools.Count; }
return turns.Count.ToString() + " turns, " + tools.ToString() + " tool calls";
}See also#
- what a task cost, and what it did (task.Calls) — what the same calls cost, readable by anyone who can read the task
- the agent task log (AgentTask) — the task itself: what caused it, when it ran, how it ended
- the agent loop (app.Agent, Loop) — why a loop-driven task's own calls are empty, and this member spans the job