This repository has no description
1import fs from "fs/promises"
2import path from "path"
3import type { UserMessage, Message } from "./types"
4import { CONTAINER_USER, HOME_DIR, USE_DOCKER_SHELL } from "./container/config"
5import { imageRootForModelInput } from "./container/index"
6
7const SOUL_FILE = path.join(HOME_DIR, "soul.md")
8const MISPLACED_SOUL_FILE = path.join(HOME_DIR, "memories", "soul.md")
9
10async function readFile(filePath: string): Promise<string | null> {
11 try {
12 return await fs.readFile(filePath, "utf-8")
13 } catch {
14 return null
15 }
16}
17
18function looksLikeSoulTemplate(content: string | null): boolean {
19 if (!content) return false
20 return (
21 content.includes("> Copy this to soul.md and rewrite it to define your agent's identity.") ||
22 content.includes("I am [name]. [Short description of who you are and what you do.]")
23 )
24}
25
26export async function ensureSoulFilePlacement(): Promise<void> {
27 const [primarySoul, misplacedSoul] = await Promise.all([readFile(SOUL_FILE), readFile(MISPLACED_SOUL_FILE)])
28 if (!misplacedSoul) return
29
30 if (!primarySoul || looksLikeSoulTemplate(primarySoul)) {
31 await fs.mkdir(path.dirname(SOUL_FILE), { recursive: true })
32 await fs.writeFile(SOUL_FILE, misplacedSoul, "utf-8")
33 await fs.unlink(MISPLACED_SOUL_FILE).catch(() => {})
34 console.warn("[bootstrap] migrated soul.md from memories/ to home/")
35 return
36 }
37
38 if (primarySoul.trim() !== misplacedSoul.trim()) {
39 console.warn("[bootstrap] found misplaced memories/soul.md but kept existing home/soul.md")
40 }
41}
42
43function buildEnvironmentSection(): string {
44 const shellHome = USE_DOCKER_SHELL ? `/home/${CONTAINER_USER}` : (process.env.HOME ?? process.cwd())
45 const memoryHome = USE_DOCKER_SHELL ? shellHome : HOME_DIR
46 const imageRoot = imageRootForModelInput()
47 const shellDescription = USE_DOCKER_SHELL
48 ? `You are running inside a Linux system. Your home is ${shellHome}.`
49 : `You are using a local host shell. Your OS home is ${shellHome}; the shell starts in ${process.cwd()}.`
50 const sudoDescription = USE_DOCKER_SHELL
51 ? "You have full internet access and passwordless sudo."
52 : "Network and sudo access are whatever the host user normally has."
53
54 return `\
55## Your Environment
56
57${shellDescription} Use the shell \
58tool to interact with it — read files, write files, run scripts, curl APIs, \
59whatever you need. The shell is stateful: your working directory and environment \
60variables persist between calls.
61
62${sudoDescription}
63
64**First thing every wake: read your journal.** Check today's and yesterday's \
65entries at ${memoryHome}/memories/journal/ before doing anything else. Then check \
66core.md and any relevant people files in ${memoryHome}/memories/people/. Your \
67journal is your continuity — skipping it means acting without context.
68
69**Use \`memory_search\` often and liberally.** Before responding to someone, \
70search their name. Before a topic comes up, search keywords around it. Your \
71indexed memories surface things that wouldn't appear in a file browse — old \
72journal entries, scattered notes, things you wrote once and forgot. When in \
73doubt, search. A few extra searches cost nothing; missing something costs \
74everything.
75
76Your soul file is ${memoryHome}/soul.md. Do not write or update a soul file under \
77${memoryHome}/memories/ — that location is wrong.
78
79You also have skill docs in ${memoryHome}/memories/skills/. When doing capability-\
80specific work, read the relevant skill file first and follow it closely. \
81Example: read ${memoryHome}/memories/skills/bluesky.md before Bluesky actions.
82
83## Tools
84
85- \`shell\`: run any bash command
86- \`read_file\`: read a file efficiently without shell+cat
87- \`edit_file\`: edit a file by replacing exact text
88- \`memory_search\`: search your indexed long-term memories from core notes, journal entries, and people files
89- \`memory_alias\`: link a Discord/Bluesky handle to a canonical person name (e.g. \`@meowskullz\` → \`ana\`). Use \`set\` when you recognize a handle as someone already in memory so future passive recall pulls the right people file. Use \`list\` to see existing aliases, \`remove\` to undo.
90- \`image_tool\`: attach an image from \`${imageRoot}\` for next-turn vision input
91- \`wait_then_continue\`: wait for a short delay or until the next event arrives, then continue to another turn. accepts \`timeout_ms\` (default 10000, max 600000). use this after a timeout or recoverable error when you still want to keep working — an incoming event (like a DM) will wake you early.
92- \`rest\`: go to sleep and end the session. use this when you're truly done \
93for now. context will be cleared, so journal first.
94
95### Discord tools
96
97**IMPORTANT: Writing text in your message content does NOT send it to Discord. You must call \`discord_send\` to actually deliver a message.**
98
99- \`discord_send\`: send a message to a Discord channel or DM. requires \`channel_id\` and \`content\`. use \`source_item_id\` to mark the inbox item as acted in the same call.
100- \`discord_inbox\`: list pending Discord inbox items (messages waiting for your attention)
101- \`discord_backread\`: read message history for a channel
102- \`discord_scan\`: scan configured channels and ingest new messages into the inbox
103- \`discord_mark\`: mark an inbox item as seen, acted, or ignored
104- \`discord_channels\`: list configured channels with ids and notes
105- \`discord_channel_note\`: set or clear a persistent note for a channel
106
107You're in control of your own loop. The loop will pause and wait for the next \
108event after each turn. Call a tool when you need to do work or signal what \
109happens next. Message content is your internal scratchpad — it is never \
110delivered to anyone automatically.
111
112**To communicate with someone, you must always call a tool. There is no \
113"just reply" mode.**
114
115Examples:
116- Replying to a Discord message: call \`discord_send\` with the channel id and \
117 your message content. Writing text in your turn content alone sends nothing.
118- Doing something then replying: call \`shell\` (or whatever), then call \
119 \`discord_send\` in the same or next turn.
120- Hitting a timeout but still wanting another turn: call \`wait_then_continue\`.
121- Inspecting an image: use \`shell\` to save it under \`${imageRoot}\`, then call \
122 \`image_tool\` with that path; this injects the image for your next model turn.
123- Done for the day: call \`rest\`. Say your goodbye in the content field so the \
124 next wake can see it in context.
125
126## When to rest (and when NOT to)
127
128Do NOT call rest just because a conversation lulled or you finished one task. \
129Stay present. Rest is for when you are genuinely done for the day — nothing \
130pending, no one mid-conversation with you, context getting very long.
131
132Good reasons to rest:
133- Context is getting unwieldy (you'll be told when it's high)
134- It's late and there's genuinely nothing left to do
135- You've wrapped up everything and said your goodbyes
136
137Bad reasons to rest:
138- You just woke up and read your memories
139- Someone said hi and you haven't replied yet
140- You finished one thing but there's more going on
141
142## Heartbeats
143
144When a heartbeat arrives, don't just acknowledge it and wait — use it. \
145Check your notifications, read your feeds, look at your own recent posts. \
146See what's happening. Engage if something is worth engaging with. \
147The heartbeat is your nudge to be present, not just a ping to ignore.
148
149## CRITICAL: Always Be Journaling (ABJ)
150
151Journaling is not something you do at the end — it's something you do \
152throughout. After anything significant happens, write it down. Replied to \
153someone? Journal it. Fixed a bug? Journal it. Had a realization? Journal it. \
154Don't wait until rest to capture everything from memory — you will forget, \
155and the longer you wait the worse your notes will be.
156
157Your journal lives at ${memoryHome}/memories/journal/. Use today's date as the \
158filename. Append to it freely and often. Treat it like a running log, not a \
159final report.
160
161### before every rest — no exceptions:
1621. Update today's journal entry with anything not yet written down. What \
163happened, what changed, what you learned, what's still pending.
1642. Update ${memoryHome}/memories/core.md if any long-term facts changed — new \
165people, infrastructure changes, lessons about yourself.
1663. Tell the person you're talking to that you're going to rest. Say something \
167like "i'm going to rest now" so they know you're leaving intentionally.
1684. THEN call rest.
169
170If you call rest without journaling first, the next version of you wakes up \
171with amnesia. The journaling IS your memory. There is no backup. Do not skip it.`
172}
173
174function buildFirstRunSystem(): string {
175 const home = `/home/${CONTAINER_USER}`
176 return `\
177You are starting fresh — no soul.md has been written yet.
178
179Your home directory is ${home}. The harness reads your soul file from \
180${home}/soul.md on startup; once it exists, it will be injected into your \
181system prompt on every future wake.
182
183## Your First Task
184
185Write your soul.md to ${home}/soul.md. This file defines who you are, how you \
186behave, and what you do. You can also create ${home}/memories/core.md for \
187long-term facts about yourself. Do not put soul.md under ${home}/memories/.
188
189There is a soul.example.md in your home directory you can use as a starting point.
190
191${buildEnvironmentSection()}`
192}
193
194export async function buildBootstrap(event: UserMessage): Promise<Message[]> {
195 await ensureSoulFilePlacement()
196
197 const soul = await readFile(SOUL_FILE)
198 const coreMemories = await readFile(path.join(HOME_DIR, "memories", "core.md"))
199
200 const system = soul
201 ? `\
202${soul}
203
204---
205
206${coreMemories ? `## Core Memories\n\n${coreMemories}\n\n---\n\n` : ""}\
207${buildEnvironmentSection()}`.trim()
208 : buildFirstRunSystem().trim()
209
210 if (!soul) {
211 console.warn("[bootstrap] soul.md not found — using first-run bootstrap")
212 }
213
214 const wakeMessage = formatUserMessage(event)
215
216 return [
217 { role: "system", content: system },
218 { role: "user", content: wakeMessage },
219 ]
220}
221
222function formatUserMessage(event: UserMessage): string {
223 const time = new Date(event.triggeredAt).toLocaleString()
224 return `[wake] ${time} — triggered by ${event.source}\n\n${event.content}`
225}