Recording from an agent
How an AI agent's actions get recorded so the record is outside the agent's reach: where the write goes, what names the agent, and what the agent can and cannot do to its own record.
An agent acts on its own: it calls tools, reads files, changes things, sends messages, hands work to a person. Afterward someone has to rely on an account of what it did and on whose instruction: the team that runs it, an outside evaluator, an auditor, a customer, an oversight office. Logs the agent's own runtime writes are within the agent's reach. A PacSpace record is not: once an entry is committed, the agent that wrote it cannot change it, and neither can the people who run it. PacSpace is the flight recorder for AI agents.
This section is for the engineer wiring an agent up, and for an agent reading it to understand the calls. The calls are the same as for any other system; what changes is where the write comes from and how the entries are named.
The shape of an agent's record
One record per run. The first entry names the run and carries the fingerprint of the instruction it was given, so the record opens with what the agent was asked to do. Each action is an entry after it, in the order it happened: a tool call, a file it read, a decision, a handoff to a person. When the run ends, close the record, and anything that tries to write after that is recorded as an attempt.
import { createReadStream } from 'node:fs';
import { PacSpace, fingerprint, readBlindingFile, writeBlindingFile } from '@pacspace-io/sdk';
const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY! }); // read in the harness; the agent never sees it
// Fingerprint a file where it is kept, and keep its blinding file beside it.
// A second call for the same file reuses that blinding file, so a retry sends the same fingerprint.
async function refOf(path: string) {
const kept = await readBlindingFile(`${path}.pacspace.json`).catch(() => undefined);
const { ref, blinding } = await fingerprint(createReadStream(path), { blinding: kept });
if (!kept && blinding) await writeBlindingFile(path, blinding);
return ref;
}
const record = `run-${runId}`;
// The run opens. Entry 1's title is the record's name.
await pac.records.emit({
record,
title: `Agent-7 support run, ${today}`,
kind: 'agent-run',
occurredAt: startedAt,
actorId: 'agent-7',
instructedBy: requestedBy,
payloads: [await refOf(`runs/${runId}/instruction.md`)],
idempotencyKey: `${record}:open`,
});
// Each action, after it happens.
await pac.records.emit({
record,
title: 'Refund of $84.00 issued on order 88214',
kind: 'agent-action',
occurredAt: refundedAt,
actorId: 'agent-7',
instructedBy: requestedBy,
payloads: [await refOf(`runs/${runId}/step-${stepId}.json`)],
idempotencyKey: `${record}:step-${stepId}`,
});
// The run closes. A write after this is recorded as an attempt.
await pac.records.emit({
record,
title: 'Run finished: 31 tickets handled, 2 handed to a person',
kind: 'agent-run-close',
lifecycle: 'closed',
occurredAt: finishedAt,
actorId: 'agent-7',
instructedBy: requestedBy,
payloads: [await refOf(`runs/${runId}/transcript.jsonl`)],
idempotencyKey: `${record}:close`,
});actorId is the agent. instructedBy is the person or system that set it going. Both are your own names for them, and both are sealed with every entry, so the record says who acted and on whose word without anyone having to remember.
What names the agent
There is no agent credential. The write carries your workspace's API key, and the key says which workspace wrote the entry. Which agent acted is named inside the entry, by you, in actorId. That is deliberate: a key is a secret, and an agent's name is a fact you want in the record. Keep the key out of the agent's context and put the agent's name in the entry. Where the write goes says how.
What the agent cannot do
An agent that writes to a record can write a false entry, and the record will hold it, with the agent's name and the time. What it cannot do is change an entry once it is committed, remove one, reorder them, or write to a closed record without the attempt being recorded. The check a reader runs, on the Shared Record or with the checker, does not ask the agent, its runtime, or PacSpace whether the record is fine; it recomputes. So the record of what an agent did is out of reach of the agent, and of anyone with the agent's access, from the moment each entry commits.
PacSpace records and never decides. It does not watch the agent, judge its actions, or stop them. A monitor of your own that stops an agent, or a person who approves before it acts, writes its own entries to the same record, and afterward there is a record, not an argument. An agent in an evaluation and What an agent did show both.
Pages in this section
| Page | What it covers |
|---|---|
| Where the write goes | Which process holds the key and makes the call, and why it is never the model. |
| Integration patterns | Entry per action, entry per run, or a run that opens and closes, and how to name the entries. |
| Safety and idempotency | Retries that never record twice, keys made from the run, and the two states a write can be left in. |
| PacSpace MCP | A preview page. Nothing on it is released. |
Failure modes
- The key in the agent's prompt, tools, or memory. Anything the model can read it can leak; the write belongs in the harness.
- An idempotency key made from the clock. A retry becomes a second entry.
- A new fingerprint of the same file on a retry. Blinded fingerprints differ each time, so the retry is a different body; reuse the blinding file, as
refOfdoes. - Recording the action before it happened.
occurredAtis when the agent acted, and an entry written before the action can describe an action that never took place. - One record for every run the agent ever makes. A record is one thing; a run is one thing.