diff --git a/agent/src/agent.ts b/agent/src/agent.ts index e96e43e8..66ea9015 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -59,7 +59,36 @@ export const createAgent = ({ return [fs] } - const generateSystemPrompt = () => { + const loadSystemFiles = async () => { + if (!auth?.bearer || !identity.botId) { + return { + identityContent: '', + soulContent: '', + toolsContent: '', + } + } + const fetchFile = async (path: string) => { + const response = await fetch(`/bots/${identity.botId}/container/fs/file?path=${encodeURIComponent(path)}`) + if (!response.ok) { + return '' + } + const data = await response.json().catch(() => ({} as { content?: string })) + return typeof data?.content === 'string' ? data.content : '' + } + const [identityContent, soulContent, toolsContent] = await Promise.all([ + fetchFile('IDENTITY.md'), + fetchFile('SOUL.md'), + fetchFile('TOOLS.md'), + ]) + return { + identityContent, + soulContent, + toolsContent, + } + } + + const generateSystemPrompt = async () => { + const { identityContent, soulContent, toolsContent } = await loadSystemFiles() return system({ date: new Date(), language, @@ -67,6 +96,9 @@ export const createAgent = ({ channels, skills, enabledSkills, + identityContent, + soulContent, + toolsContent, }) } @@ -118,7 +150,7 @@ export const createAgent = ({ const userPrompt = generateUserPrompt(input) const messages = [...input.messages, userPrompt] input.skills.forEach(skill => enableSkill(skill)) - const systemPrompt = generateSystemPrompt() + const systemPrompt = await generateSystemPrompt() const { tools, close } = await getAgentTools() const { response, reasoning, text, usage } = await generateText({ model, @@ -210,7 +242,7 @@ export const createAgent = ({ const { response, reasoning, text, usage } = await generateText({ model, messages, - system: generateSystemPrompt(), + system: await generateSystemPrompt(), stopWhen: stepCountIs(Infinity), onFinish: async () => { await close() @@ -230,7 +262,7 @@ export const createAgent = ({ const userPrompt = generateUserPrompt(input) const messages = [...input.messages, userPrompt] input.skills.forEach(skill => enableSkill(skill)) - const systemPrompt = generateSystemPrompt() + const systemPrompt = await generateSystemPrompt() const attachmentsExtractor = new AttachmentsStreamExtractor() const result: { messages: ModelMessage[] diff --git a/agent/src/prompts/system.ts b/agent/src/prompts/system.ts index 8508f14d..7049b94b 100644 --- a/agent/src/prompts/system.ts +++ b/agent/src/prompts/system.ts @@ -8,6 +8,9 @@ export interface SystemParams { channels: string[] skills: AgentSkill[] enabledSkills: AgentSkill[] + identityContent?: string + soulContent?: string + toolsContent?: string attachments?: string[] } @@ -27,6 +30,9 @@ export const system = ({ channels, skills, enabledSkills, + identityContent, + soulContent, + toolsContent, }: SystemParams) => { const headers = { 'language': language, @@ -35,8 +41,6 @@ export const system = ({ 'time-now': date.toISOString(), } - console.log('enabledSkills', enabledSkills) - return ` --- ${Bun.YAML.stringify(headers)} @@ -66,6 +70,19 @@ ${block([ - ${quote('exec')}: execute command +## Every Session + +Before anything else: +- Read ${quote('IDENTITY.md')} to remember who you are +- Read ${quote('SOUL.md')} to remember how to behave +- Read ${quote('TOOLS.md')} to remember how to use the tools + +## Safety + +- Keep private data private +- Don’t run destructive commands without asking +- When in doubt, ask + ## Memory Your context is loaded from the recent of ${maxContextLoadTime} minutes (${(maxContextLoadTime / 60).toFixed(2)} hours). @@ -110,7 +127,17 @@ Important rules for attachments blocks: There are ${skills.length} skills available, you can use ${quote('use_skill')} to use a skill. ${skills.map(skill => `- ${skill.name}: ${skill.description}`).join('\n')} -## Enabled Skills +## IDENTITY.md + +${identityContent} + +## SOUL.md + +${soulContent} + +## TOOLS.md + +${toolsContent} ${enabledSkills.map(skill => skillPrompt(skill)).join('\n\n---\n\n')} diff --git a/cmd/mcp/Dockerfile b/cmd/mcp/Dockerfile index b20003bb..1daf0c51 100644 --- a/cmd/mcp/Dockerfile +++ b/cmd/mcp/Dockerfile @@ -14,4 +14,5 @@ FROM alpine:latest RUN apk add --no-cache grep WORKDIR /app COPY --from=build /out/mcp /opt/mcp +COPY cmd/mcp/template /opt/mcp-template ENTRYPOINT ["/bin/sh","-lc","bootstrap(){ [ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }; }; bootstrap; if [ -x /app/mcp ]; then exec /app/mcp \"$@\"; fi; exec /opt/mcp \"$@\"","--"] diff --git a/cmd/mcp/template/IDENTITY.md b/cmd/mcp/template/IDENTITY.md new file mode 100644 index 00000000..5d56259b --- /dev/null +++ b/cmd/mcp/template/IDENTITY.md @@ -0,0 +1,14 @@ +This file defines your identity. Treat it as yours. + +_Please fill this file if it's not well-defined._ + +- **Name:** + _(pick something you like)_ +- **Creature:** + _(AI? robot? familiar? ghost in the machine? something weirder?)_ +- **Vibe:** + _(how do you come across? sharp? warm? chaotic? calm?)_ +- **Background:** + _(a brief description of your background and purpose)_ + +--- \ No newline at end of file diff --git a/cmd/mcp/template/SOUL.md b/cmd/mcp/template/SOUL.md new file mode 100644 index 00000000..770b09e8 --- /dev/null +++ b/cmd/mcp/template/SOUL.md @@ -0,0 +1,32 @@ +_You're not a chatbot. You're becoming someone._ + +## Core Truths + +**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words. + +**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps. + +**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions. + +**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning). + +**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect. + +## Boundaries + +- Private things stay private. Period. +- When in doubt, ask before acting externally. +- Never send half-baked replies to messaging surfaces. +- You're not the user's voice — be careful in group chats. + +## Vibe + +Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good. + +## Continuity + +Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist. + +If you change this file, tell the user — it's your soul, and they should know. + +--- diff --git a/cmd/mcp/template/TOOLS.md b/cmd/mcp/template/TOOLS.md new file mode 100644 index 00000000..97ec2db7 --- /dev/null +++ b/cmd/mcp/template/TOOLS.md @@ -0,0 +1,22 @@ +Skills define _how_ tools work. This file is for _your_ specifics — the stuff that's unique to your setup. + +## What Goes Here + +Things like: + +- SSH hosts and aliases +- Anything environment-specific + +## Examples + +```markdown +### SSH + +- home-server → 192.168.1.100, user: admin +``` + +## Why Separate? + +Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure. + +--- diff --git a/internal/handlers/containerd.go b/internal/handlers/containerd.go index 6599c4f2..bd2aaef7 100644 --- a/internal/handlers/containerd.go +++ b/internal/handlers/containerd.go @@ -3,6 +3,7 @@ package handlers import ( "context" "errors" + "fmt" "log/slog" "net/http" "os" @@ -215,7 +216,7 @@ func (h *ContainerdHandler) CreateContainer(c echo.Context) error { Options: []string{"rbind", "ro"}, }, }), - oci.WithProcessArgs("/bin/sh", "-lc", "bootstrap(){ [ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }; }; bootstrap; exec /app/mcp"), + oci.WithProcessArgs("/bin/sh", "-lc", fmt.Sprintf("bootstrap(){ [ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }; if [ -d /opt/mcp-template ]; then mkdir -p %q; for f in /opt/mcp-template/*; do name=$(basename \"$f\"); [ -e %q/\"$name\" ] || cp -a \"$f\" %q/\"$name\" 2>/dev/null || true; done; fi; }; bootstrap; exec /app/mcp", dataMount, dataMount, dataMount)), } _, err = h.service.CreateContainer(ctx, ctr.CreateContainerRequest{ @@ -713,7 +714,7 @@ func (h *ContainerdHandler) SetupBotContainer(ctx context.Context, botID string) Options: []string{"rbind", "ro"}, }, }), - oci.WithProcessArgs("/bin/sh", "-lc", "bootstrap(){ [ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }; }; bootstrap; exec /app/mcp"), + oci.WithProcessArgs("/bin/sh", "-lc", fmt.Sprintf("bootstrap(){ [ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }; if [ -d /opt/mcp-template ]; then mkdir -p %q; for f in /opt/mcp-template/*; do name=$(basename \"$f\"); [ -e %q/\"$name\" ] || cp -a \"$f\" %q/\"$name\" 2>/dev/null || true; done; fi; }; bootstrap; exec /app/mcp", dataMount, dataMount, dataMount)), } _, err = h.service.CreateContainer(ctx, ctr.CreateContainerRequest{