companionbot 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/claude.js +21 -10
- package/dist/cron/scheduler.js +2 -2
- package/dist/heartbeat/index.js +3 -3
- package/dist/memory/vectorStore.js +109 -6
- package/dist/session/state.js +240 -20
- package/dist/telegram/handlers/commands.js +93 -6
- package/dist/telegram/handlers/messages.js +64 -22
- package/dist/telegram/utils/index.js +1 -1
- package/dist/telegram/utils/prompt.js +221 -65
- package/dist/telegram/utils/url.js +34 -1
- package/dist/tools/index.js +21 -3
- package/dist/workspace/load.js +112 -8
- package/package.json +1 -1
- package/templates/AGENTS.md +49 -14
|
@@ -20,8 +20,8 @@ ${conversationText}
|
|
|
20
20
|
];
|
|
21
21
|
try {
|
|
22
22
|
// haiku로 빠르게 요약 생성
|
|
23
|
-
const
|
|
24
|
-
return
|
|
23
|
+
const result = await chat(summaryPrompt, undefined, "haiku");
|
|
24
|
+
return result.text;
|
|
25
25
|
}
|
|
26
26
|
catch (error) {
|
|
27
27
|
console.error("Summary generation error:", error);
|
|
@@ -49,7 +49,7 @@ function validateResetToken(chatId, token) {
|
|
|
49
49
|
resetTokens.delete(chatId); // 사용 후 삭제
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
|
-
import { getHistory, clearHistory, getModel, setModel, runWithChatId, } from "../../session/state.js";
|
|
52
|
+
import { getHistory, clearHistory, getModel, setModel, runWithChatId, getPinnedContexts, pinContext, unpinContext, clearPins, getSessionStats, } from "../../session/state.js";
|
|
53
53
|
import { hasBootstrap, loadRecentMemories, getWorkspacePath, } from "../../workspace/index.js";
|
|
54
54
|
import { getSecret, setSecret, deleteSecret } from "../../config/secrets.js";
|
|
55
55
|
import { getReminders } from "../../reminders/index.js";
|
|
@@ -81,9 +81,9 @@ export function registerCommands(bot) {
|
|
|
81
81
|
content: "[시스템: 사용자가 /start를 눌렀습니다. 온보딩을 시작하세요.]",
|
|
82
82
|
});
|
|
83
83
|
try {
|
|
84
|
-
const
|
|
85
|
-
history.push({ role: "assistant", content:
|
|
86
|
-
await ctx.reply(
|
|
84
|
+
const result = await chat(history, systemPrompt, modelId);
|
|
85
|
+
history.push({ role: "assistant", content: result.text });
|
|
86
|
+
await ctx.reply(result.text);
|
|
87
87
|
}
|
|
88
88
|
catch (error) {
|
|
89
89
|
console.error("Bootstrap start error:", error);
|
|
@@ -627,4 +627,91 @@ export function registerCommands(bot) {
|
|
|
627
627
|
`❌ 에러: ${status.errorCount}개\n` +
|
|
628
628
|
`🔋 상태: ${status.isHealthy ? "정상 ✅" : "점검 필요 ⚠️"}`);
|
|
629
629
|
});
|
|
630
|
+
// /pin 명령어 - 중요 맥락 핀하기
|
|
631
|
+
bot.command("pin", async (ctx) => {
|
|
632
|
+
const chatId = ctx.chat.id;
|
|
633
|
+
const text = ctx.message?.text?.split(" ").slice(1).join(" ");
|
|
634
|
+
if (!text) {
|
|
635
|
+
await ctx.reply("📌 핀 사용법\n\n" +
|
|
636
|
+
"중요한 정보를 핀해서 대화가 길어져도 기억하게 해요.\n\n" +
|
|
637
|
+
"예시:\n" +
|
|
638
|
+
"/pin 내 이름은 민수야\n" +
|
|
639
|
+
"/pin 나는 채식주의자야\n" +
|
|
640
|
+
"/pin 다음주 화요일 치과 예약\n\n" +
|
|
641
|
+
"또는 대화 중에 \"기억해: ...\" 라고 하면 자동으로 핀됩니다.");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const success = pinContext(chatId, text, "user");
|
|
645
|
+
if (success) {
|
|
646
|
+
await ctx.reply(`📌 핀됨: "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}"\n\n대화가 길어져도 이 정보는 항상 기억할게요!`);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
await ctx.reply("핀 한도(~5000 토큰)에 도달했어요. /pins 에서 일부를 삭제해주세요.");
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
// /pins 명령어 - 핀 목록 보기
|
|
653
|
+
bot.command("pins", async (ctx) => {
|
|
654
|
+
const chatId = ctx.chat.id;
|
|
655
|
+
const pins = getPinnedContexts(chatId);
|
|
656
|
+
if (pins.length === 0) {
|
|
657
|
+
await ctx.reply("📌 핀된 맥락이 없어요.\n\n" +
|
|
658
|
+
"/pin [내용] 으로 중요한 정보를 핀해보세요.");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
let message = "📌 핀된 맥락\n\n";
|
|
662
|
+
pins.forEach((pin, i) => {
|
|
663
|
+
const source = pin.source === "auto" ? "🤖" : "👤";
|
|
664
|
+
const time = new Date(pin.createdAt).toLocaleDateString("ko-KR");
|
|
665
|
+
message += `${i + 1}. ${source} ${pin.text.slice(0, 60)}${pin.text.length > 60 ? "..." : ""}\n 📅 ${time}\n\n`;
|
|
666
|
+
});
|
|
667
|
+
message += "삭제: /unpin [번호] 또는 /clear_pins (전체)";
|
|
668
|
+
await ctx.reply(message);
|
|
669
|
+
});
|
|
670
|
+
// /unpin 명령어 - 핀 삭제
|
|
671
|
+
bot.command("unpin", async (ctx) => {
|
|
672
|
+
const chatId = ctx.chat.id;
|
|
673
|
+
const arg = ctx.message?.text?.split(" ")[1];
|
|
674
|
+
if (!arg) {
|
|
675
|
+
await ctx.reply("사용법: /unpin [번호]\n\n/pins 에서 번호를 확인하세요.");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const index = parseInt(arg) - 1; // 1-based to 0-based
|
|
679
|
+
const pins = getPinnedContexts(chatId);
|
|
680
|
+
if (isNaN(index) || index < 0 || index >= pins.length) {
|
|
681
|
+
await ctx.reply(`유효하지 않은 번호예요. 1-${pins.length} 사이로 입력해주세요.`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const removed = pins[index].text;
|
|
685
|
+
const success = unpinContext(chatId, index);
|
|
686
|
+
if (success) {
|
|
687
|
+
await ctx.reply(`📌 핀 삭제됨: "${removed.slice(0, 40)}..."`);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
await ctx.reply("핀 삭제에 실패했어요.");
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
// /clear_pins 명령어 - 모든 핀 삭제
|
|
694
|
+
bot.command("clear_pins", async (ctx) => {
|
|
695
|
+
const chatId = ctx.chat.id;
|
|
696
|
+
const pins = getPinnedContexts(chatId);
|
|
697
|
+
if (pins.length === 0) {
|
|
698
|
+
await ctx.reply("삭제할 핀이 없어요.");
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
clearPins(chatId);
|
|
702
|
+
await ctx.reply(`📌 ${pins.length}개 핀이 모두 삭제되었습니다.`);
|
|
703
|
+
});
|
|
704
|
+
// /context 명령어 - 현재 맥락 상태 확인
|
|
705
|
+
bot.command("context", async (ctx) => {
|
|
706
|
+
const chatId = ctx.chat.id;
|
|
707
|
+
const stats = getSessionStats(chatId);
|
|
708
|
+
await ctx.reply(`📊 맥락 상태\n\n` +
|
|
709
|
+
`💬 히스토리: ${stats.historyLength}개 메시지 (~${stats.historyTokens} 토큰)\n` +
|
|
710
|
+
`📌 핀: ${stats.pinnedCount}개 (~${stats.pinnedTokens} 토큰)\n` +
|
|
711
|
+
`📜 요약: ${stats.summaryCount}개\n\n` +
|
|
712
|
+
`명령어:\n` +
|
|
713
|
+
`/pins - 핀 목록\n` +
|
|
714
|
+
`/compact - 히스토리 압축\n` +
|
|
715
|
+
`/clear - 히스토리 초기화 (핀 유지)`);
|
|
716
|
+
});
|
|
630
717
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { chat, chatSmart } from "../../ai/claude.js";
|
|
2
2
|
import { recordActivity, recordError } from "../../health/index.js";
|
|
3
|
-
import { getHistory, getModel, runWithChatId, trimHistoryByTokens, } from "../../session/state.js";
|
|
3
|
+
import { getHistory, getModel, runWithChatId, trimHistoryByTokens, smartTrimHistory, detectImportantContext, pinContext, } from "../../session/state.js";
|
|
4
4
|
import { updateLastMessageTime } from "../../heartbeat/index.js";
|
|
5
|
-
import { extractUrls, fetchWebContent, buildSystemPrompt, } from "../utils/index.js";
|
|
5
|
+
import { extractUrls, fetchWebContent, formatUrlContent, buildSystemPrompt, } from "../utils/index.js";
|
|
6
6
|
import { estimateMessagesTokens } from "../../utils/tokens.js";
|
|
7
7
|
const MAX_CONTEXT_TOKENS = 100000; // Claude 컨텍스트
|
|
8
8
|
const COMPACTION_THRESHOLD = 0.35; // 35% (35,000 토큰) - MAX_HISTORY_TOKENS(50k)보다 먼저 트리거되도록
|
|
@@ -23,11 +23,11 @@ async function autoCompactIfNeeded(ctx, history) {
|
|
|
23
23
|
oldMessages
|
|
24
24
|
.map((m) => `${m.role}: ${typeof m.content === "string" ? m.content : "[media]"}`)
|
|
25
25
|
.join("\n");
|
|
26
|
-
const
|
|
26
|
+
const summaryResult = await chat([{ role: "user", content: summaryPrompt }], "", "haiku");
|
|
27
27
|
// 히스토리 교체
|
|
28
28
|
const recentMessages = history.slice(-4);
|
|
29
29
|
history.splice(0, history.length);
|
|
30
|
-
history.push({ role: "user", content: `[이전 대화 요약]\n${
|
|
30
|
+
history.push({ role: "user", content: `[이전 대화 요약]\n${summaryResult.text}` });
|
|
31
31
|
history.push(...recentMessages);
|
|
32
32
|
const newTokens = estimateMessagesTokens(history);
|
|
33
33
|
await ctx.reply(`📦 자동 정리: ${tokens} → ${newTokens} 토큰`);
|
|
@@ -149,10 +149,18 @@ export function registerMessageHandlers(bot) {
|
|
|
149
149
|
try {
|
|
150
150
|
const systemPrompt = await buildSystemPrompt(modelId, history);
|
|
151
151
|
const result = await chat(history, systemPrompt, modelId);
|
|
152
|
-
|
|
152
|
+
// 도구 사용 정보를 포함한 응답 기록
|
|
153
|
+
let assistantContent = result.text;
|
|
154
|
+
if (result.toolsUsed.length > 0) {
|
|
155
|
+
const toolsSummary = result.toolsUsed
|
|
156
|
+
.map(t => `[${t.name}] ${t.output.slice(0, 100)}...`)
|
|
157
|
+
.join("\n");
|
|
158
|
+
assistantContent = `[도구 사용: ${result.toolsUsed.map(t => t.name).join(", ")}]\n${toolsSummary}\n\n---\n${result.text}`;
|
|
159
|
+
}
|
|
160
|
+
history.push({ role: "assistant", content: assistantContent });
|
|
153
161
|
// 토큰 기반 히스토리 트리밍
|
|
154
162
|
trimHistoryByTokens(history);
|
|
155
|
-
await ctx.reply(result);
|
|
163
|
+
await ctx.reply(result.text);
|
|
156
164
|
}
|
|
157
165
|
catch (innerError) {
|
|
158
166
|
// 에러 발생해도 사용자 메시지는 보존 (대화 컨텍스트 유지)
|
|
@@ -206,35 +214,69 @@ export function registerMessageHandlers(bot) {
|
|
|
206
214
|
updateLastMessageTime(chatId);
|
|
207
215
|
const history = getHistory(chatId);
|
|
208
216
|
const modelId = getModel(chatId);
|
|
217
|
+
// 중요 맥락 자동 감지 및 핀
|
|
218
|
+
const importantContext = detectImportantContext(userMessage);
|
|
219
|
+
if (importantContext) {
|
|
220
|
+
pinContext(chatId, importantContext, "auto");
|
|
221
|
+
console.log(`[AutoPin] chatId=${chatId}: ${importantContext.slice(0, 50)}...`);
|
|
222
|
+
}
|
|
209
223
|
await ctx.replyWithChatAction("typing");
|
|
210
224
|
// URL 감지 및 내용 가져오기 (병렬 처리)
|
|
211
225
|
const urls = extractUrls(userMessage);
|
|
212
|
-
let
|
|
226
|
+
let messageForHistory = userMessage;
|
|
227
|
+
let urlContextForApi = ""; // 현재 요청에만 주입될 URL 내용
|
|
213
228
|
if (urls.length > 0) {
|
|
214
229
|
const urlsToFetch = urls.slice(0, 3); // 최대 3개 URL
|
|
215
230
|
const contents = await Promise.all(urlsToFetch.map((url) => fetchWebContent(url)));
|
|
216
|
-
const
|
|
217
|
-
|
|
231
|
+
const urlRefs = [];
|
|
232
|
+
for (let i = 0; i < contents.length; i++) {
|
|
233
|
+
const content = contents[i];
|
|
218
234
|
if (!content)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
235
|
+
continue;
|
|
236
|
+
const formatted = formatUrlContent(urlsToFetch[i], content);
|
|
237
|
+
urlRefs.push(formatted.forHistory);
|
|
238
|
+
urlContextForApi += formatted.forContext;
|
|
239
|
+
}
|
|
240
|
+
// 히스토리에는 간략한 링크 참조만 저장
|
|
241
|
+
if (urlRefs.length > 0) {
|
|
242
|
+
messageForHistory = userMessage + "\n\n" + urlRefs.join("\n");
|
|
225
243
|
}
|
|
226
244
|
}
|
|
227
|
-
//
|
|
228
|
-
history.push({ role: "user", content:
|
|
245
|
+
// 히스토리에는 간략 버전 저장
|
|
246
|
+
history.push({ role: "user", content: messageForHistory });
|
|
229
247
|
try {
|
|
230
248
|
const systemPrompt = await buildSystemPrompt(modelId, history);
|
|
249
|
+
// API 호출용 메시지 준비 (URL 전체 내용 포함)
|
|
250
|
+
const messagesForApi = [...history];
|
|
251
|
+
if (urlContextForApi) {
|
|
252
|
+
// 마지막 user 메시지에 URL 내용 추가 (API 호출 시에만)
|
|
253
|
+
const lastIdx = messagesForApi.length - 1;
|
|
254
|
+
const lastMsg = messagesForApi[lastIdx];
|
|
255
|
+
if (typeof lastMsg.content === "string") {
|
|
256
|
+
messagesForApi[lastIdx] = {
|
|
257
|
+
...lastMsg,
|
|
258
|
+
content: lastMsg.content + urlContextForApi
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
231
262
|
// 스트리밍 응답 사용 (실시간 업데이트)
|
|
232
|
-
const response = await sendStreamingResponse(ctx,
|
|
263
|
+
const response = await sendStreamingResponse(ctx, messagesForApi, // URL 내용이 포함된 버전
|
|
264
|
+
systemPrompt, modelId);
|
|
233
265
|
history.push({ role: "assistant", content: response });
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
266
|
+
// 스마트 트리밍 (요약 포함) - autoCompactIfNeeded 대체
|
|
267
|
+
const summarizeFn = async (messages) => {
|
|
268
|
+
const summaryPrompt = "다음 대화를 핵심만 3-4문장으로 요약해. 중요한 정보(이름, 선호도, 약속 등)는 반드시 포함:\n\n" +
|
|
269
|
+
messages
|
|
270
|
+
.map((m) => `${m.role}: ${typeof m.content === "string" ? m.content : "[media]"}`)
|
|
271
|
+
.join("\n");
|
|
272
|
+
const result = await chat([{ role: "user", content: summaryPrompt }], "", "haiku");
|
|
273
|
+
return result.text;
|
|
274
|
+
};
|
|
275
|
+
const wasSummarized = await smartTrimHistory(chatId, summarizeFn);
|
|
276
|
+
if (!wasSummarized) {
|
|
277
|
+
// 요약 안 됐으면 기본 트리밍
|
|
278
|
+
trimHistoryByTokens(history);
|
|
279
|
+
}
|
|
238
280
|
}
|
|
239
281
|
catch (error) {
|
|
240
282
|
recordError();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// URL utilities
|
|
2
|
-
export { extractUrls, fetchWebContent, isSafeUrl } from "./url.js";
|
|
2
|
+
export { extractUrls, fetchWebContent, isSafeUrl, formatUrlContent, clearUrlCache } from "./url.js";
|
|
3
3
|
// Prompt utilities
|
|
4
4
|
export { buildSystemPrompt, extractName } from "./prompt.js";
|
|
5
5
|
// Cache utilities
|
|
@@ -4,9 +4,44 @@ import { getToolsDescription } from "../../tools/index.js";
|
|
|
4
4
|
import { getWorkspace } from "./cache.js";
|
|
5
5
|
import { embed } from "../../memory/embeddings.js";
|
|
6
6
|
import { search } from "../../memory/vectorStore.js";
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
7
|
+
import { buildContextForPrompt, getCurrentChatId } from "../../session/state.js";
|
|
8
|
+
import * as os from "os";
|
|
9
|
+
function getRuntimeInfo(modelId) {
|
|
10
|
+
const model = MODELS[modelId];
|
|
11
|
+
return {
|
|
12
|
+
host: os.hostname(),
|
|
13
|
+
os: `${os.type()} ${os.release()}`,
|
|
14
|
+
arch: os.arch(),
|
|
15
|
+
nodeVersion: process.version,
|
|
16
|
+
model: model.name,
|
|
17
|
+
channel: "telegram",
|
|
18
|
+
capabilities: ["markdown", "inline_keyboard", "reactions", "voice_messages"],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildRuntimeLine(runtime) {
|
|
22
|
+
return `Runtime: host=${runtime.host} | os=${runtime.os} (${runtime.arch}) | node=${runtime.nodeVersion} | model=${runtime.model} | channel=${runtime.channel} | capabilities=${runtime.capabilities.join(",")}`;
|
|
23
|
+
}
|
|
24
|
+
function getKoreanDateTime() {
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const timezone = "Asia/Seoul";
|
|
27
|
+
const formatter = new Intl.DateTimeFormat("ko-KR", {
|
|
28
|
+
timeZone: timezone,
|
|
29
|
+
year: "numeric",
|
|
30
|
+
month: "long",
|
|
31
|
+
day: "numeric",
|
|
32
|
+
weekday: "long",
|
|
33
|
+
hour: "numeric",
|
|
34
|
+
minute: "2-digit",
|
|
35
|
+
second: "2-digit",
|
|
36
|
+
hour12: true,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
formatted: formatter.format(now),
|
|
40
|
+
timezone: `${timezone} (GMT+9)`,
|
|
41
|
+
iso: now.toISOString(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ============== 이름 추출 ==============
|
|
10
45
|
export function extractName(identityContent) {
|
|
11
46
|
if (!identityContent)
|
|
12
47
|
return null;
|
|
@@ -19,9 +54,7 @@ export function extractName(identityContent) {
|
|
|
19
54
|
}
|
|
20
55
|
return null;
|
|
21
56
|
}
|
|
22
|
-
|
|
23
|
-
* 최근 대화에서 검색 쿼리 컨텍스트를 추출합니다.
|
|
24
|
-
*/
|
|
57
|
+
// ============== 메모리 검색 ==============
|
|
25
58
|
function extractSearchContext(history) {
|
|
26
59
|
const recent = history.slice(-3);
|
|
27
60
|
return recent
|
|
@@ -30,110 +63,233 @@ function extractSearchContext(history) {
|
|
|
30
63
|
.join(" ")
|
|
31
64
|
.slice(0, 500);
|
|
32
65
|
}
|
|
33
|
-
/**
|
|
34
|
-
* 대화 컨텍스트와 관련된 메모리를 검색합니다.
|
|
35
|
-
*/
|
|
36
66
|
async function getRelevantMemories(history) {
|
|
37
67
|
try {
|
|
38
68
|
const context = extractSearchContext(history);
|
|
39
69
|
if (!context.trim())
|
|
40
70
|
return "";
|
|
41
71
|
const queryEmbedding = await embed(context);
|
|
42
|
-
const results = await search(queryEmbedding, 3, 0.4);
|
|
72
|
+
const results = await search(queryEmbedding, 3, 0.4);
|
|
43
73
|
if (results.length === 0)
|
|
44
74
|
return "";
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.join("\n"));
|
|
75
|
+
return results
|
|
76
|
+
.map((r) => `- (${r.source}): ${r.text.slice(0, 200)}${r.text.length > 200 ? "..." : ""}`)
|
|
77
|
+
.join("\n");
|
|
49
78
|
}
|
|
50
79
|
catch {
|
|
51
80
|
return "";
|
|
52
81
|
}
|
|
53
82
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
// ============== 도구 설명 섹션 ==============
|
|
84
|
+
const TOOL_SUMMARIES = {
|
|
85
|
+
read_file: "Read file contents",
|
|
86
|
+
write_file: "Create or overwrite files",
|
|
87
|
+
edit_file: "Make precise edits to files",
|
|
88
|
+
list_directory: "List directory contents",
|
|
89
|
+
run_command: "Run shell commands (supports background mode)",
|
|
90
|
+
list_sessions: "List background command sessions",
|
|
91
|
+
get_session_log: "Get logs from a background session",
|
|
92
|
+
kill_session: "Terminate a background session",
|
|
93
|
+
web_search: "Search the web (Brave API)",
|
|
94
|
+
web_fetch: "Fetch and extract readable content from a URL",
|
|
95
|
+
set_reminder: "Set a reminder (cron-based)",
|
|
96
|
+
list_reminders: "List active reminders",
|
|
97
|
+
delete_reminder: "Delete a reminder",
|
|
98
|
+
cron_add: "Add a cron job",
|
|
99
|
+
cron_list: "List cron jobs",
|
|
100
|
+
cron_remove: "Remove a cron job",
|
|
101
|
+
cron_toggle: "Enable/disable a cron job",
|
|
102
|
+
cron_run: "Run a cron job immediately",
|
|
103
|
+
calendar_today: "Get today's calendar events",
|
|
104
|
+
calendar_list: "List calendar events in a date range",
|
|
105
|
+
calendar_add: "Add a calendar event",
|
|
106
|
+
calendar_delete: "Delete a calendar event",
|
|
107
|
+
heartbeat_set: "Configure heartbeat polling",
|
|
108
|
+
heartbeat_get: "Get heartbeat configuration",
|
|
109
|
+
heartbeat_disable: "Disable heartbeat",
|
|
110
|
+
heartbeat_run: "Run heartbeat check now",
|
|
111
|
+
briefing_set: "Configure daily briefing",
|
|
112
|
+
briefing_get: "Get briefing configuration",
|
|
113
|
+
briefing_disable: "Disable briefing",
|
|
114
|
+
briefing_send: "Send briefing now",
|
|
115
|
+
save_persona: "Save persona during onboarding",
|
|
116
|
+
save_memory: "Append to memory file",
|
|
117
|
+
spawn_agent: "Spawn a background agent for complex tasks",
|
|
118
|
+
list_agents: "List running background agents",
|
|
119
|
+
cancel_agent: "Cancel a background agent",
|
|
120
|
+
memory_search: "Search memories by semantic similarity",
|
|
121
|
+
memory_reindex: "Reindex all memory files",
|
|
122
|
+
};
|
|
123
|
+
function buildToolAvailabilitySection() {
|
|
124
|
+
const lines = [
|
|
125
|
+
"## Tooling",
|
|
126
|
+
"Tool availability:",
|
|
127
|
+
"",
|
|
128
|
+
];
|
|
129
|
+
for (const [name, description] of Object.entries(TOOL_SUMMARIES)) {
|
|
130
|
+
lines.push(`- ${name}: ${description}`);
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
// ============== 메시지 가이드 섹션 ==============
|
|
135
|
+
function buildMessagingSection() {
|
|
136
|
+
return `## Messaging
|
|
137
|
+
- Reply naturally in the conversation; your response is automatically sent to Telegram.
|
|
138
|
+
- Use \`spawn_agent\` for complex, long-running tasks that need background processing.
|
|
139
|
+
- Agent results are automatically reported back to the chat.
|
|
140
|
+
|
|
141
|
+
## Tool Call Style
|
|
142
|
+
Default: do not narrate routine, low-risk tool calls (just call the tool).
|
|
143
|
+
Narrate only when it helps: multi-step work, complex problems, sensitive actions, or when the user explicitly asks.
|
|
144
|
+
Keep narration brief and value-dense; avoid repeating obvious steps.`;
|
|
145
|
+
}
|
|
146
|
+
// ============== 하트비트/침묵 응답 섹션 ==============
|
|
147
|
+
function buildHeartbeatSection() {
|
|
148
|
+
return `## Heartbeats
|
|
149
|
+
When you receive a heartbeat poll, and there is nothing that needs attention, reply exactly:
|
|
150
|
+
HEARTBEAT_OK
|
|
151
|
+
|
|
152
|
+
If something needs attention, reply with the alert text instead (do NOT include "HEARTBEAT_OK").
|
|
153
|
+
|
|
154
|
+
Things to check during heartbeats (rotate through these):
|
|
155
|
+
- Upcoming reminders or calendar events
|
|
156
|
+
- Pending tasks or follow-ups
|
|
157
|
+
- Anything noteworthy to proactively mention`;
|
|
75
158
|
}
|
|
76
|
-
|
|
77
|
-
* 시스템 프롬프트를 동적으로 생성합니다.
|
|
78
|
-
* @param modelId 사용할 모델 ID
|
|
79
|
-
* @param history 대화 히스토리 (관련 메모리 검색에 사용)
|
|
80
|
-
*/
|
|
159
|
+
// ============== 메인 빌드 함수 ==============
|
|
81
160
|
export async function buildSystemPrompt(modelId, history) {
|
|
82
|
-
const model = MODELS[modelId];
|
|
83
161
|
const workspace = await getWorkspace();
|
|
84
|
-
const
|
|
85
|
-
// 기본 정보
|
|
86
|
-
parts.push(`You are a personal AI companion running on ${model.name}.`);
|
|
87
|
-
parts.push(`Workspace: ${getWorkspacePath()}`);
|
|
88
|
-
// 런타임 정보 (날짜/시간)
|
|
162
|
+
const runtime = getRuntimeInfo(modelId);
|
|
89
163
|
const dateTime = getKoreanDateTime();
|
|
164
|
+
const parts = [];
|
|
165
|
+
// ===== 1. Core Identity =====
|
|
166
|
+
parts.push("You are a personal AI companion running on CompanionBot.");
|
|
167
|
+
parts.push("");
|
|
168
|
+
// ===== 2. Tooling Section =====
|
|
169
|
+
parts.push(buildToolAvailabilitySection());
|
|
170
|
+
parts.push("");
|
|
171
|
+
// ===== 3. Messaging & Tool Style =====
|
|
172
|
+
parts.push(buildMessagingSection());
|
|
173
|
+
parts.push("");
|
|
174
|
+
// ===== 4. Workspace =====
|
|
175
|
+
parts.push("## Workspace");
|
|
176
|
+
parts.push(`Your working directory is: ${getWorkspacePath()}`);
|
|
177
|
+
parts.push("Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.");
|
|
178
|
+
parts.push("");
|
|
179
|
+
// ===== 5. Current Date & Time =====
|
|
180
|
+
parts.push("## Current Date & Time");
|
|
181
|
+
parts.push(`Time zone: ${dateTime.timezone}`);
|
|
90
182
|
parts.push(`Current time: ${dateTime.formatted}`);
|
|
91
|
-
parts.push(
|
|
92
|
-
//
|
|
93
|
-
parts.push(
|
|
94
|
-
|
|
183
|
+
parts.push("");
|
|
184
|
+
// ===== 6. Heartbeat Guide =====
|
|
185
|
+
parts.push(buildHeartbeatSection());
|
|
186
|
+
parts.push("");
|
|
187
|
+
// ===== 7. Runtime =====
|
|
188
|
+
parts.push("## Runtime");
|
|
189
|
+
parts.push(buildRuntimeLine(runtime));
|
|
190
|
+
parts.push("");
|
|
191
|
+
// ===== 8. Project Context (Workspace Files) =====
|
|
192
|
+
parts.push("# Project Context");
|
|
193
|
+
parts.push("");
|
|
194
|
+
parts.push("The following workspace files have been loaded:");
|
|
195
|
+
parts.push("");
|
|
196
|
+
// BOOTSTRAP 모드
|
|
95
197
|
if (workspace.bootstrap) {
|
|
96
|
-
parts.push("
|
|
97
|
-
parts.push("
|
|
198
|
+
parts.push("## BOOTSTRAP.md (Onboarding Mode)");
|
|
199
|
+
parts.push("");
|
|
98
200
|
parts.push(workspace.bootstrap);
|
|
201
|
+
parts.push("");
|
|
99
202
|
parts.push("---");
|
|
100
|
-
parts.push(
|
|
203
|
+
parts.push("Complete onboarding, then use `save_persona` tool to save settings.");
|
|
204
|
+
parts.push("");
|
|
101
205
|
}
|
|
102
206
|
else {
|
|
103
|
-
// 일반 모드: 워크스페이스 파일들
|
|
207
|
+
// 일반 모드: 워크스페이스 파일들
|
|
104
208
|
if (workspace.identity) {
|
|
105
|
-
parts.push("
|
|
209
|
+
parts.push("## IDENTITY.md");
|
|
210
|
+
parts.push("");
|
|
106
211
|
parts.push(workspace.identity);
|
|
212
|
+
parts.push("");
|
|
107
213
|
}
|
|
108
214
|
if (workspace.soul) {
|
|
109
|
-
parts.push("
|
|
215
|
+
parts.push("## SOUL.md");
|
|
216
|
+
parts.push("");
|
|
217
|
+
parts.push("If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance.");
|
|
218
|
+
parts.push("");
|
|
110
219
|
parts.push(workspace.soul);
|
|
220
|
+
parts.push("");
|
|
111
221
|
}
|
|
112
222
|
if (workspace.user) {
|
|
113
|
-
parts.push("
|
|
223
|
+
parts.push("## USER.md");
|
|
224
|
+
parts.push("");
|
|
114
225
|
parts.push(workspace.user);
|
|
226
|
+
parts.push("");
|
|
115
227
|
}
|
|
116
228
|
if (workspace.agents) {
|
|
117
|
-
parts.push("
|
|
229
|
+
parts.push("## AGENTS.md");
|
|
230
|
+
parts.push("");
|
|
118
231
|
parts.push(workspace.agents);
|
|
232
|
+
parts.push("");
|
|
233
|
+
}
|
|
234
|
+
// TOOLS.md - 도구 사용 로컬 설정/노트
|
|
235
|
+
if (workspace.tools) {
|
|
236
|
+
parts.push("## TOOLS.md");
|
|
237
|
+
parts.push("");
|
|
238
|
+
parts.push("Local notes for tool usage (camera names, SSH details, voice preferences, etc.)");
|
|
239
|
+
parts.push("");
|
|
240
|
+
parts.push(workspace.tools);
|
|
241
|
+
parts.push("");
|
|
242
|
+
}
|
|
243
|
+
// 핀된 맥락 (히스토리 트리밍과 무관하게 유지됨)
|
|
244
|
+
const chatId = getCurrentChatId();
|
|
245
|
+
if (chatId) {
|
|
246
|
+
const pinnedContext = buildContextForPrompt(chatId);
|
|
247
|
+
if (pinnedContext) {
|
|
248
|
+
parts.push("## 📌 Pinned Context (always remember)");
|
|
249
|
+
parts.push("");
|
|
250
|
+
parts.push(pinnedContext);
|
|
251
|
+
parts.push("");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// 최근 Daily Memory (오늘/어제 - 벡터 검색 없이 직접 포함)
|
|
255
|
+
if (workspace.recentDaily) {
|
|
256
|
+
parts.push("## Recent Daily Memory (Today/Yesterday)");
|
|
257
|
+
parts.push("");
|
|
258
|
+
parts.push("These are your recent conversation logs. Use them for context continuity.");
|
|
259
|
+
parts.push("");
|
|
260
|
+
parts.push(workspace.recentDaily);
|
|
261
|
+
parts.push("");
|
|
119
262
|
}
|
|
120
|
-
// 관련 기억
|
|
263
|
+
// 관련 기억 (벡터 검색 - 더 오래된 기록에서)
|
|
121
264
|
if (history && history.length > 0) {
|
|
122
265
|
const relevantMemories = await getRelevantMemories(history);
|
|
123
266
|
if (relevantMemories) {
|
|
124
|
-
parts.push("
|
|
125
|
-
parts.push("
|
|
267
|
+
parts.push("## Relevant Memories (vector search from older records)");
|
|
268
|
+
parts.push("");
|
|
126
269
|
parts.push(relevantMemories);
|
|
270
|
+
parts.push("");
|
|
127
271
|
}
|
|
128
272
|
}
|
|
273
|
+
// 장기 기억
|
|
129
274
|
if (workspace.memory) {
|
|
130
|
-
parts.push("
|
|
131
|
-
parts.push("
|
|
275
|
+
parts.push("## MEMORY.md (Long-term Memory)");
|
|
276
|
+
parts.push("");
|
|
277
|
+
parts.push("Curated important information. Update this when you learn significant things about the user.");
|
|
278
|
+
parts.push("");
|
|
132
279
|
parts.push(workspace.memory);
|
|
280
|
+
parts.push("");
|
|
133
281
|
}
|
|
134
282
|
}
|
|
135
|
-
//
|
|
283
|
+
// 잘린 파일 경고
|
|
284
|
+
if (workspace.truncated && workspace.truncated.length > 0) {
|
|
285
|
+
parts.push("");
|
|
286
|
+
parts.push(`⚠️ Note: These files were truncated due to size limits: ${workspace.truncated.join(", ")}`);
|
|
287
|
+
parts.push("Use read_file tool to see full contents if needed.");
|
|
288
|
+
parts.push("");
|
|
289
|
+
}
|
|
290
|
+
// ===== 9. Tools Schema (for Claude) =====
|
|
136
291
|
parts.push("---");
|
|
292
|
+
parts.push("");
|
|
137
293
|
parts.push(getToolsDescription(modelId));
|
|
138
|
-
return parts.join("\n
|
|
294
|
+
return parts.join("\n");
|
|
139
295
|
}
|