# what the agent saw (task.Transcript)

> What an agent's model calls actually contained — the system prompt as the model received it, the messages sent, the response that came back, and every tool the turn invoked with its input and output. Unlike a task's costs, content is not readable just because you can read the task: the agent declares who may see what it saw, and an agent that declares nobody is readable by nobody.

<!-- id: agent-task-transcript · area: agent · stability: preview · html: https://osysharp.com/reference/agent/task-transcript/ -->

## Summary        {#summary}

[`task.Calls`](https://osysharp.com/reference/agent/task-calls/) tells you what a task *cost*. This tells you what it *contained*.

```osy syntax
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      {#signature}

```osy syntax
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    {#description}

### Who can read it — you must say, or nobody can   {#security}

Say it on the agent, in its `security { }` block:

```osy syntax
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`](https://osysharp.com/reference/agent/task-log/) 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?   {#subtree}

`Transcript` includes the turns of everything the task set off, not just its own — because a task driven by a
[loop](https://osysharp.com/reference/agent/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   {#withheld}

A turn whose `ContentWithheld` is true happened and cost what [its call](https://osysharp.com/reference/agent/task-calls/) 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:

```osy syntax
foreach (var turn in task.Transcript) {
  if (turn.ContentWithheld) { /* "not recorded" */ }
  else { /* turn.SystemPrompt, turn.Messages, turn.Response */ }
}
```

## Examples       {#examples}

A review screen showing what the agent was told and what it did about it:

```osy title="what-the-agent-saw" test app=agent-task-transcript
using Osysharp.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       {#see-also}
- [what a task cost, and what it did (task.Calls)](https://osysharp.com/reference/agent/task-calls/) — what the same calls *cost*, readable by anyone who can read the task
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — the task itself: what caused it, when it ran, how it ended
- [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) — why a loop-driven task's own calls are empty, and this member spans the job
