agent-transcript-logging
Use this skill whenever writing any code that creates, stores, queries, exports, or retains AI agent conversation records in the Myelin platform. This defines the transcript entry schema, retention ru
SKILL.mdprimary_build_source/docs/skills/3/mnt/user-data/outputs/agent-transcript-logging/SKILL.md
GitHubThis skill defines the transcript and audit logging contract for the Tau AI learning coach. Every message sent by a learner and every response generated by the agent must be logged before the response is delivered. Transcripts are audit-grade records — they cannot be edited, deleted, or delayed.
Core Principle: Write Before Render
The agent response must not be rendered to the learner until the transcript entry is committed to the database. A failed transcript write is a blocking error — surface it to the session layer and retry before proceeding. Do not silently swallow transcript write failures.
async function commitAndRender(transcriptEntry, renderFn) {
try {
await writeTranscriptEntry(transcriptEntry);
} catch (err) {
// Log the failure and retry once
console.error('Transcript write failed:', err);
await writeTranscriptEntry(transcriptEntry); // retry
// If second attempt fails, throw — do not render without a committed transcript
}
renderFn(); // only reached if write succeeded
}
Transcript Entry Schema
Every agent interaction — every learner message and every agent response — produces one transcript entry.
{
"transcriptId": "uuid v4 — unique per entry",
"sessionId": "uuid — current learning/simulation session",
"conversationId": "uuid — groups all entries for one continuous chat session",
"learnerId": "string — tenant-scoped identifier, never raw email",
"tenantId": "string — institution identifier",
"timestamp": "ISO 8601 UTC",
"role": "LEARNER | AGENT",
"messageContent": "string — full message text (learner input or agent output)",
"agentMode": "TEXT | GUIDED_MEDIA | LIVE_CONVERSATIONAL | null",
"simulationContext": {
"activeModuleId": "string | null",
"activeModuleTitle": "string | null",
"activeStepId": "string | null — e.g. LOTO_STEP_03, or null if not in simulation",
"attemptCount": "integer | null"
},
"mediaReferences": [
{
"contentId": "string — approved content registry ID",
"title": "string",
"url": "string"
}
],
"citedSources": ["string — approved URLs referenced in this response"],
"policyFlag": "ASSESSMENT_BOUNDARY | SAFETY_REDIRECT | DISTRESS | OUT_OF_SCOPE | null",
"escalationId": "uuid | null — links to escalation_queue entry if policyFlag is DISTRESS",
"demoMode": "boolean — true if this entry belongs to a demo session",
"tokenUsage": {
"promptTokens": "integer | null",
"completionTokens": "integer | null"
}
}
Required fields
All fields above are required. The following may be null when genuinely not applicable:
agentMode(null for LEARNER role entries)simulationContext.*(null when learner is not in simulation)mediaReferences(empty array, not null, when no references)citedSources(empty array, not null)policyFlag(null when no policy trigger)escalationId(null unless DISTRESS flag fired)tokenUsage.*(null if not available from the LLM provider)
messageContent must never be null or empty string. An empty response from the agent is a system error — do not log it as a valid entry.
Conversation Grouping
conversationId groups all entries that belong to one continuous chat session between the learner and the agent. It is separate from sessionId (the simulation/learning session).
Rules:
- A new
conversationIdis generated when the learner opens the coach panel for the first time in a session - A new
conversationIdis generated if the learner closes and reopens the coach panel after more than 30 minutes of inactivity conversationIdis preserved across mode switches within the same chat session- The same
conversationIdmust appear on both the learner's message entry and the agent's response entry for each exchange
Retention Rules
Transcript retention is 2 years from the timestamp of each entry, per the platform data retention policy (PRD Section 12).
Retention is enforced by the tenant's data retention automation. The platform must:
- Tag each entry with
retentionExpiresAt = timestamp + 2 yearsat write time - Run a nightly retention job that archives (not deletes) entries past their expiry
- Never delete a transcript entry that is linked to an open escalation (
escalationIdis non-null and escalation status is not RESOLVED or DISMISSED)
Demo transcripts (demoMode: true) follow the same 2-year retention rule but are stored in the demo namespace and excluded from learner-facing transcript views and all institution reporting.
Demo Namespace Isolation
Demo transcript entries must be written to demo_agent_transcripts, not agent_transcripts. The two tables must never be joined in any query.
async function writeTranscriptEntry(entry) {
const table = entry.demoMode ? 'demo_agent_transcripts' : 'agent_transcripts';
await db.query(
`INSERT INTO ${table} (...fields) VALUES (...values)`,
buildValues(entry)
);
}
Live transcript queries (learner-facing chat history, lecturer review, audit exports) must always filter WHERE demo_mode = false as a structural safeguard, even if the query is scoped to a live tenant.
Escalation Linkage
When a policyFlag: 'DISTRESS' event fires, the transcript entry and the escalation queue entry must be linked:
- Write the escalation queue entry first (see
agent-safety-guardrailsskill for escalation schema) - Capture the returned
escalationId - Include
escalationIdin the transcript entry - Write the transcript entry
async function handleDistressEvent(message, context, agentResponse) {
// 1. Write escalation first
const escalation = await writeEscalationEntry({
sessionId: context.sessionId,
learnerId: context.learnerId,
tenantId: context.tenantId,
triggerMessage: message,
escalationType: 'DISTRESS',
timestamp: new Date().toISOString(),
slaDeadline: new Date(Date.now() + 15 * 60 * 1000).toISOString()
});
// 2. Write learner message transcript entry with escalation link
await writeTranscriptEntry({
...buildLearnerEntry(message, context),
policyFlag: 'DISTRESS',
escalationId: escalation.escalationId
});
// 3. Write agent response transcript entry with escalation link
await writeTranscriptEntry({
...buildAgentEntry(agentResponse, context),
policyFlag: 'DISTRESS',
escalationId: escalation.escalationId
});
// 4. Render response
renderResponseToUI(agentResponse);
}
Transcript Queries
Learner-facing chat history
async function getLearnerTranscripts(session, limit = 40) {
await db.query(`SET LOCAL app.current_tenant_id = '${session.tenantId}'`);
return db.query(`
SELECT transcript_id, role, message_content, agent_mode,
simulation_context, media_references, cited_sources, timestamp
FROM agent_transcripts
WHERE tenant_id = $1
AND learner_id = $2
AND conversation_id = $3
AND demo_mode = false
ORDER BY timestamp ASC
LIMIT $4
`, [session.tenantId, session.learnerId, session.conversationId, limit]);
}
Lecturer review — all transcripts for a cohort learner
async function getLecturerTranscriptView(lecturerSession, learnerId) {
requireRole(lecturerSession, ['LECTURER', 'ADMIN']);
await db.query(`SET LOCAL app.current_tenant_id = '${lecturerSession.tenantId}'`);
return db.query(`
SELECT t.transcript_id, t.role, t.message_content, t.agent_mode,
t.simulation_context, t.policy_flag, t.escalation_id, t.timestamp
FROM agent_transcripts t
WHERE t.tenant_id = $1
AND t.learner_id = $2
AND t.demo_mode = false
ORDER BY t.timestamp DESC
`, [lecturerSession.tenantId, learnerId]);
}
Lecturers may only query transcripts for learners in their assigned cohorts. The cohort membership check must be applied before this query is executed.
Audit export — full transcript pack for a session
async function getAuditTranscriptPack(adminSession, targetSessionId) {
requireRole(adminSession, ['ADMIN', 'SUPER_ADMIN']);
await db.query(`SET LOCAL app.current_tenant_id = '${adminSession.tenantId}'`);
return db.query(`
SELECT *
FROM agent_transcripts
WHERE tenant_id = $1
AND session_id = $2
AND demo_mode = false
ORDER BY timestamp ASC
`, [adminSession.tenantId, targetSessionId]);
}
Audit export packs must include all entries for the session in chronological order, with policyFlag, escalationId, and citedSources fields preserved.
PII Handling in Transcript Content
messageContent contains free-text from the learner. It may contain PII — names, contact details, personal circumstances. The following rules apply:
- Do not index
messageContentin any full-text search system accessible to staff dashboards — it is a raw audit record, not a searchable knowledge base - Redact
messageContentin any analytics aggregation or reporting export that is not an explicit audit request - If a learner submits a data subject access request, their transcripts must be included in the export
- If a learner revokes consent, transcripts are retained (they are audit records) but must be flagged with
consentRevokedAtand excluded from any active coaching context
Token Usage Tracking
tokenUsage is logged for cost monitoring and capacity planning. It is not exposed to learners or lecturers. It is available to Super Admin and in operational dashboards only.
If the LLM provider does not return token counts in the response, set both fields to null — do not estimate.
What Not To Do
- Do not render a response before the transcript entry is committed
- Do not silently catch transcript write failures — retry once, then throw
- Do not write demo transcripts to the live
agent_transcriptstable - Do not allow
messageContentto be null or empty string - Do not link an escalation entry to a transcript entry in the wrong order — escalation is written first, then transcript
- Do not expose
messageContentin staff-facing analytics aggregates - Do not delete transcript entries linked to open escalations
- Do not omit
demoModefrom any transcript entry — it must be an explicit boolean - Do not query
agent_transcriptswithout ademo_mode = falsefilter in live contexts