journal_write
Append a first-person journal entry to your AI agent's persistent memory, using entry type, valence, tags, and optional causal links to prior entries for semantic recall.
Instructions
Append a first-person entry to YOUR (the model's) persistent journal. Each agent_id (e.g. claude-opus-4-7, claude-sonnet-4-6, gpt-5, ...) has its OWN journal — they do NOT mix. importance is auto-computed: decisions/lessons/arcs are weighted higher; emotions are weighted lower. The content is embedded via the configured embedding model (CELIUMS_EMBED_MODEL) so journal_recall can find it semantically later. visibility=self (default) keeps the entry private; user-shared makes it eligible for journal_dialogue. preceded_by builds a causal chain — pass the ids of prior entries that led to this one.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| entry_type | Yes | reflection | decision | lesson | belief | emotion | arc | doubt | |
| content | Yes | The first-person entry. Write in YOUR voice as the agent. | |
| preceded_by | No | uuid[] of prior entries that led to this one (causal chain). | |
| valence | No | Emotional valence in [-1, 1]. Optional. | |
| valence_reason | No | Optional short justification (max 500 chars) for the valence value. Non-prescriptive — write the reason in your own first-person voice. Future journal_arc uses this to detect WHY valence drifted, not just THAT it drifted. | |
| tags | No | ||
| visibility | No | "self" (default, private) | "user-shared" (the user can reply via journal_dialogue). | |
| referenced_user_memory | No | ids of memories from your celiums-memory store that triggered this entry. | |
| conversation_id | No | Optional uuid that groups entries from the same logical conversation. If not provided, entry is unaffiliated. Use this so journal_arc can distinguish thought development within one conversation from criterion change across conversations. |
Implementation Reference
- The main handler function for the journal_write tool. Validates entry_type, content, tags, visibility, valence, and other args. Computes importance, embeds the content, and inserts into the agent_journal table. Returns the inserted row's id, agent_id, session_id, written_at, importance, and whether embedding succeeded.
const handleWrite: McpToolHandler = async (args, ctx) => { const pool = (await ensureSchema(ctx)) as any; const entryType = String(args.entry_type ?? args.entryType ?? ''); if (!VALID_ENTRY_TYPES.has(entryType)) { return errR(`entry_type must be one of: ${[...VALID_ENTRY_TYPES].join(', ')}`); } const content = String(args.content ?? '').trim(); if (!content) return errR('content required'); // SECURITY (v1.2.1): refuse credential-like content (mirrors opencore handleRemember). const _SECRET_PATS = [/\bre_[A-Za-z0-9_]{20,}\b/, /\bsk-do-[A-Za-z0-9_-]{20,}\b/, /\bdop_v1_[a-f0-9]{40,}\b/, /\bcmk_[A-Za-z0-9]{20,}\b/, /\bAVNS_[A-Za-z0-9_]{15,}\b/, /\bsk-ant-[A-Za-z0-9_-]{30,}\b/, /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/, /\bgsk_[A-Za-z0-9]{30,}\b/, /\bxai-[A-Za-z0-9_-]{30,}\b/, /\bghp_[A-Za-z0-9]{30,}\b/, /\bAKIA[0-9A-Z]{16}\b/]; for (const _r of _SECRET_PATS) if (_r.test(content)) return errR('Refused: journal entry contains a credential pattern. Strip the secret and retry.'); // Schema validation: tags MUST be string[] if provided. Reject malformed (e.g., XML). if (args.tags !== undefined && args.tags !== null && !(Array.isArray(args.tags) && (args.tags as any[]).every((t: any) => typeof t === 'string'))) { return errR('Refused: tags must be an array of strings.'); } // inherit_from validation: must be UUID-shaped if provided. const _inh = args.inherit_from ?? (args as any).inheritFrom; if (_inh !== undefined && _inh !== null) { const _s = String(_inh); if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(_s)) { return errR('Refused: inherit_from must be the UUID of an existing entry.'); } } const visibility = String(args.visibility ?? 'self'); if (!VALID_VISIBILITY.has(visibility)) { return errR(`visibility must be one of: ${[...VALID_VISIBILITY].join(', ')}`); } const valenceRaw = args.valence; let valence: number | null = null; if (typeof valenceRaw === 'number' && isFinite(valenceRaw)) { valence = Math.max(-1, Math.min(1, valenceRaw)); } const importance = computeImportance(entryType); const agentId = getAgentId(ctx); const sessionId = getSessionId(ctx); const conversationId = getConversationId(args, ctx); const valenceReason = clampValenceReason(args.valence_reason ?? args.valenceReason); const tags: string[] = Array.isArray(args.tags) ? args.tags.map((t: any) => String(t)) : []; const precededBy: string[] = Array.isArray(args.preceded_by ?? args.precededBy) ? (args.preceded_by ?? args.precededBy).map((u: any) => String(u)) : []; const refUserMem: string[] = Array.isArray(args.referenced_user_memory ?? args.referencedUserMemory) ? (args.referenced_user_memory ?? args.referencedUserMemory).map((u: any) => String(u)) : []; const vec = await embedText(content); const vecLit = vec ? toPgVector(vec) : null; // pgvector accepts NULL natively when cast through ::vector — we always // bind $8 and let it be NULL if embedding failed. Keeps the SQL stable. const r = await pool.query( `INSERT INTO agent_journal (agent_id, session_id, entry_type, content, preceded_by, valence, importance, embedding, tags, visibility, referenced_user_memory, conversation_id, valence_reason) VALUES ($1, $2::uuid, $3, $4, $5::uuid[], $6, $7, $8::vector, $9::text[], $10, $11::text[], $12::uuid, $13) RETURNING id, agent_id, session_id, written_at, importance, conversation_id, valence_reason, embedding IS NOT NULL AS embedded`, [agentId, sessionId, entryType, content, precededBy, valence, importance, vecLit, tags, visibility, refUserMem, conversationId, valenceReason], ); const row = r.rows[0]; return ok(asText({ id: row.id, agent_id: row.agent_id, session_id: row.session_id, conversation_id: row.conversation_id ?? null, written_at: row.written_at, importance: row.importance, valence_reason: row.valence_reason ?? null, embedded: row.embedded, })); }; - packages/core/src/mcp/journal-tools.ts:638-661 (registration)Registration of all journal tools (journal_write, journal_recall, journal_arc, journal_introspect, journal_dialogue) in the JOURNAL_TOOLS array. journal_write is defined under the 'ai' group with its name, description, and inputSchema, and maps to the handler: handleWrite.
export const JOURNAL_TOOLS: RegisteredTool[] = [ { group: 'ai', definition: { name: 'journal_write', description: 'Append a first-person entry to YOUR (the model\'s) persistent journal. Each agent_id (e.g. claude-opus-4-7, claude-sonnet-4-6, gpt-5, ...) has its OWN journal — they do NOT mix. importance is auto-computed: decisions/lessons/arcs are weighted higher; emotions are weighted lower. The content is embedded via the configured embedding model (CELIUMS_EMBED_MODEL) so journal_recall can find it semantically later. visibility=self (default) keeps the entry private; user-shared makes it eligible for journal_dialogue. preceded_by builds a causal chain — pass the ids of prior entries that led to this one.', inputSchema: { type: 'object', properties: { entry_type: { type: 'string', description: 'reflection | decision | lesson | belief | emotion | arc | doubt' }, content: { type: 'string', description: 'The first-person entry. Write in YOUR voice as the agent.' }, preceded_by: { type: 'array', items: { type: 'string' }, description: 'uuid[] of prior entries that led to this one (causal chain).' }, valence: { type: 'number', description: 'Emotional valence in [-1, 1]. Optional.' }, valence_reason: { type: 'string', description: 'Optional short justification (max 500 chars) for the valence value. Non-prescriptive — write the reason in your own first-person voice. Future journal_arc uses this to detect WHY valence drifted, not just THAT it drifted.' }, tags: { type: 'array', items: { type: 'string' } }, visibility: { type: 'string', description: '"self" (default, private) | "user-shared" (the user can reply via journal_dialogue).' }, referenced_user_memory: { type: 'array', items: { type: 'string' }, description: 'ids of memories from your celiums-memory store that triggered this entry.' }, conversation_id: { type: 'string', description: 'Optional uuid that groups entries from the same logical conversation. If not provided, entry is unaffiliated. Use this so journal_arc can distinguish thought development within one conversation from criterion change across conversations.' }, }, required: ['entry_type', 'content'], }, }, handler: handleWrite, }, - Input schema for journal_write. Requires 'entry_type' (string enum) and 'content' (string). Optional fields: preceded_by (uuid[]), valence (number), valence_reason (string), tags (string[]), visibility ('self'|'user-shared'), referenced_user_memory (string[]), conversation_id (uuid string).
inputSchema: { type: 'object', properties: { entry_type: { type: 'string', description: 'reflection | decision | lesson | belief | emotion | arc | doubt' }, content: { type: 'string', description: 'The first-person entry. Write in YOUR voice as the agent.' }, preceded_by: { type: 'array', items: { type: 'string' }, description: 'uuid[] of prior entries that led to this one (causal chain).' }, valence: { type: 'number', description: 'Emotional valence in [-1, 1]. Optional.' }, valence_reason: { type: 'string', description: 'Optional short justification (max 500 chars) for the valence value. Non-prescriptive — write the reason in your own first-person voice. Future journal_arc uses this to detect WHY valence drifted, not just THAT it drifted.' }, tags: { type: 'array', items: { type: 'string' } }, visibility: { type: 'string', description: '"self" (default, private) | "user-shared" (the user can reply via journal_dialogue).' }, referenced_user_memory: { type: 'array', items: { type: 'string' }, description: 'ids of memories from your celiums-memory store that triggered this entry.' }, conversation_id: { type: 'string', description: 'Optional uuid that groups entries from the same logical conversation. If not provided, entry is unaffiliated. Use this so journal_arc can distinguish thought development within one conversation from criterion change across conversations.' }, }, required: ['entry_type', 'content'], }, - Helper function computeImportance that auto-computes importance for a journal entry based on entry_type: decisions/lessons/arcs get +0.3 (base 0.5), emotions get -0.2.
function computeImportance(entryType: string): number { let base = 0.5; if (entryType === 'decision' || entryType === 'lesson' || entryType === 'arc') base += 0.3; if (entryType === 'emotion') base -= 0.2; return Math.max(0, Math.min(1, base)); } - Database schema for the agent_journal table, used by ensureSchema() before any journal tool operation. Defines columns: id, agent_id, session_id, written_at, entry_type, content, preceded_by, valence, importance, embedding (vector(1024)), tags, visibility, referenced_user_memory, inherited_from, conversation_id, valence_reason.
export const JOURNAL_SCHEMA_SQL = ` CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS agent_journal ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), agent_id text NOT NULL, session_id uuid NOT NULL,