@bernierllc/agent-changeset
v0.2.0
Published
Records agent-tool mutations per conversation and renders per-change summaries and end-of-conversation rollups
Readme
@bernierllc/agent-changeset
Records mutations made by agent tools during a conversation and produces structured user-facing change reports. Accumulates ChangeRecord entries (dot-notation actions, before/after state) per conversation, renders per-change one-line summaries for tool-result cards, computes field-level diffs, and produces an end-of-conversation rollup summary (markdown + plain text). Serializable via toJSON()/fromJSON() for persistence alongside conversation messages.
Designed to be installed as an @bernierllc/agent-tool-registry after-hook.
Installation
npm install @bernierllc/agent-changesetQuick Start
import { createChangesetRecorder } from '@bernierllc/agent-changeset';
const recorder = createChangesetRecorder();
const changeset = recorder.forConversation('conv-abc123');
// Record mutations from tool calls
changeset.record({
action: 'admin.service.add',
resourceType: 'service',
resourceId: 'svc-999',
after: { name: 'Haircut', price: 45, durationMinutes: 30 },
summary: 'Added service "Haircut" at $45 (30 min)',
toolCallId: 'tc-001',
success: true,
});
changeset.record({
action: 'admin.discount.create',
resourceType: 'discount',
resourceId: 'disc-42',
after: { code: 'SAVE10', percentOff: 10 },
summary: 'Created discount code "SAVE10" (10% off)',
toolCallId: 'tc-002',
success: true,
});
// Per-change line summary (for tool-result cards in the UI)
const [first] = changeset.changes;
console.log(changeset.renderLineSummary(first.id));
// '+ Added service "Haircut" at $45 (30 min)'
// End-of-conversation rollup
const rollup = changeset.renderRollupSummary();
console.log(rollup.markdown);
// ### Changes Made This Session
// **Service** (1 change)
// - Added service "Haircut" at $45 (30 min)
//
// **Discount** (1 change)
// - Created discount code "SAVE10" (10% off)
console.log(rollup.plainText);
// 'Here is what changed this session: Service: Added service "Haircut" at $45 (30 min). Discount: Created discount code "SAVE10" (10% off).'
// Persist alongside conversation messages
const json = changeset.toJSON();
// store json to your database / agent-conversation-store ...
// Restore from JSON on reconnect
const restored = recorder.fromJSON(json);Field-Level Diffs
For changes that carry both before and after, computeDiff() reports added keys, removed keys, and modified fields:
changeset.record({
action: 'admin.service.update',
resourceType: 'service',
resourceId: 'svc-999',
before: { name: 'Haircut', price: 40, durationMinutes: 30 },
after: { name: 'Haircut', price: 45, durationMinutes: 30, active: true },
summary: 'Updated service "Haircut" — price $40 to $45, added active flag',
toolCallId: 'tc-003',
success: true,
});
const updateChange = changeset.changes[2];
const diff = changeset.computeDiff(updateChange.id);
// {
// added: ['active'],
// removed: [],
// modified: [{ field: 'price', from: 40, to: 45 }]
// }Returns null for create-only (no before) or delete-only (no after) mutations.
After-Hook Pattern
import { createChangesetRecorder } from '@bernierllc/agent-changeset';
const recorder = createChangesetRecorder();
const changeset = recorder.forConversation(conversationId);
toolRegistry.addHook({
after: async (record, _ctx) => {
if (record.state === 'success' && record.output?.success === true) {
changeset.record({
action: resolveAction(record.toolName),
resourceType: record.toolName,
toolCallId: record.toolCallId,
summary: String(record.output.message ?? record.toolName),
success: true,
});
}
},
});API Reference
createChangesetRecorder(): ChangesetRecorder
Entry point. Returns a recorder that creates and restores ConversationChangeset instances.
ChangesetRecorder
| Method | Description |
|---|---|
| forConversation(id) | Create a fresh changeset for the conversation. |
| fromJSON(data) | Restore from SerializedChangeset. Throws InvalidChangesetDataError if malformed. |
ConversationChangeset
| Method | Description |
|---|---|
| record(entry) | Add a mutation. Auto-generates id (UUID v4) and timestamp (ISO 8601). Returns the stored ChangeRecord. |
| renderLineSummary(changeId) | One-liner: "+ summary" for success, "- summary" for failure. Throws ChangeNotFoundError if unknown id. |
| computeDiff(changeId) | Field-level diff from before/after. Returns null if either is absent. Throws ChangeNotFoundError if unknown id. |
| renderRollupSummary() | End-of-conversation rollup with totalChanges, successfulChanges, failedChanges, byResourceType, markdown, plainText. |
| toJSON() | Plain SerializedChangeset for persistence. |
| changes | Read-only ordered array of recorded ChangeRecords. |
| conversationId | The conversation identifier. |
Error Classes
| Class | Code | When thrown |
|---|---|---|
| ChangesetError | CHANGESET_ERROR | Base class for all package errors. |
| ChangeNotFoundError | CHANGE_NOT_FOUND | renderLineSummary or computeDiff called with unknown changeId. |
| InvalidChangesetDataError | INVALID_CHANGESET_DATA | fromJSON called with malformed data. |
All errors chain the root cause via Error.cause and carry a context object with debugging details.
Architecture
- Core tier — pure TypeScript, zero runtime dependencies beyond
@bernierllc/logger. - No I/O — pure in-memory computation; runs server-side as part of the agent service layer.
- MECE — owns only mutation recording and diff rendering for agent conversations. Persistence is
agent-conversation-store's concern. Database audit logging isadmin-agent-service's concern.
License
Bernier LLC limited-use license. See LICENSE.
