companionbot 0.13.1 → 0.15.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.
@@ -8,6 +8,11 @@
8
8
  */
9
9
  import Anthropic from "@anthropic-ai/sdk";
10
10
  import { randomUUID } from "crypto";
11
+ // ===== 제한 상수 =====
12
+ const MAX_CONCURRENT_AGENTS = 10; // 전체 동시 Agent 최대 개수
13
+ const MAX_AGENTS_PER_CHAT = 3; // chatId당 최대 동시 Agent 개수
14
+ const CLEANUP_INTERVAL_MS = 30 * 60 * 1000; // 30분마다 cleanup
15
+ const AGENT_TTL_MS = 30 * 60 * 1000; // Agent 보관 시간 (30분)
11
16
  // Agent 저장소
12
17
  const agents = new Map();
13
18
  // AbortController 저장소 (실행 중인 API 호출 취소용)
@@ -28,10 +33,50 @@ function getClient() {
28
33
  export function setAgentBot(bot) {
29
34
  botInstance = bot;
30
35
  }
36
+ /**
37
+ * 가장 오래된 Agent 정리 (한도 초과 시)
38
+ */
39
+ function evictOldestAgent() {
40
+ let oldest = null;
41
+ for (const agent of agents.values()) {
42
+ if (!oldest || agent.createdAt < oldest.createdAt) {
43
+ oldest = agent;
44
+ }
45
+ }
46
+ if (oldest) {
47
+ console.log(`[AgentManager] Evicting oldest agent: ${oldest.id}`);
48
+ // running이면 취소
49
+ if (oldest.status === "running") {
50
+ cancelAgent(oldest.id);
51
+ }
52
+ agents.delete(oldest.id);
53
+ }
54
+ }
55
+ /**
56
+ * chatId당 Agent 개수 확인
57
+ */
58
+ function countAgentsForChat(chatId) {
59
+ let count = 0;
60
+ for (const agent of agents.values()) {
61
+ if (agent.chatId === chatId && agent.status === "running") {
62
+ count++;
63
+ }
64
+ }
65
+ return count;
66
+ }
31
67
  /**
32
68
  * Sub-agent 생성 및 실행
33
69
  */
34
70
  export async function spawnAgent(task, chatId) {
71
+ // chatId당 제한 확인
72
+ const chatAgentCount = countAgentsForChat(chatId);
73
+ if (chatAgentCount >= MAX_AGENTS_PER_CHAT) {
74
+ throw new Error(`이 채팅에서 동시에 실행 가능한 Agent 수(${MAX_AGENTS_PER_CHAT}개)를 초과했습니다. 기존 Agent 완료를 기다려주세요.`);
75
+ }
76
+ // 전체 한도 확인 및 정리
77
+ while (agents.size >= MAX_CONCURRENT_AGENTS) {
78
+ evictOldestAgent();
79
+ }
35
80
  const id = randomUUID().slice(0, 8);
36
81
  const agent = {
37
82
  id,
@@ -41,6 +86,7 @@ export async function spawnAgent(task, chatId) {
41
86
  createdAt: new Date(),
42
87
  };
43
88
  agents.set(id, agent);
89
+ console.log(`[AgentManager] Agent created: ${id} (total: ${agents.size}/${MAX_CONCURRENT_AGENTS})`);
44
90
  // 비동기로 agent 실행 (await 하지 않음)
45
91
  runAgent(agent).catch((err) => {
46
92
  console.error(`[Agent ${id}] Error:`, err);
@@ -183,35 +229,47 @@ export function getAgent(agentId) {
183
229
  return agents.get(agentId);
184
230
  }
185
231
  /**
186
- * 오래된 agent 정리 (1시간 이상)
187
- * - 완료된 agent: completedAt 기준 1시간
188
- * - running 상태도 createdAt 기준 1시간 지나면 정리 (stuck 방지)
232
+ * 오래된 agent 정리 (30분 이상)
233
+ * - 완료된 agent: completedAt 기준 30분
234
+ * - running 상태도 createdAt 기준 30분 지나면 정리 (stuck 방지)
189
235
  */
190
236
  export function cleanupOldAgents() {
191
- const oneHourAgo = Date.now() - 60 * 60 * 1000;
237
+ const cutoff = Date.now() - AGENT_TTL_MS;
238
+ let cleaned = 0;
192
239
  for (const [id, agent] of agents.entries()) {
193
240
  // 완료된 agent: completedAt 기준
194
- if (agent.completedAt && agent.completedAt.getTime() < oneHourAgo) {
241
+ if (agent.completedAt && agent.completedAt.getTime() < cutoff) {
195
242
  agents.delete(id);
243
+ cleaned++;
196
244
  continue;
197
245
  }
198
- // running 상태도 1시간 지나면 정리 (stuck agent 방지)
199
- if (agent.status === "running" && agent.createdAt.getTime() < oneHourAgo) {
200
- console.log(`[Agent ${id}] Cleaning up stuck agent (running > 1h)`);
246
+ // running 상태도 TTL 지나면 정리 (stuck agent 방지)
247
+ if (agent.status === "running" && agent.createdAt.getTime() < cutoff) {
248
+ console.log(`[Agent ${id}] Cleaning up stuck agent (running > 30min)`);
249
+ // 실행 중인 API 호출 취소
250
+ const controller = abortControllers.get(id);
251
+ if (controller) {
252
+ controller.abort();
253
+ abortControllers.delete(id);
254
+ }
201
255
  agents.delete(id);
256
+ cleaned++;
202
257
  }
203
258
  }
259
+ if (cleaned > 0) {
260
+ console.log(`[AgentManager] Cleanup: removed ${cleaned} agents (remaining: ${agents.size})`);
261
+ }
204
262
  }
205
263
  // Cleanup interval 참조 저장
206
264
  let cleanupIntervalId = null;
207
265
  /**
208
- * 정기 cleanup 시작
266
+ * 정기 cleanup 시작 (30분 주기)
209
267
  */
210
268
  export function startCleanup() {
211
269
  if (cleanupIntervalId)
212
270
  return; // 이미 실행 중
213
- cleanupIntervalId = setInterval(cleanupOldAgents, 10 * 60 * 1000);
214
- console.log("[AgentManager] Cleanup interval started");
271
+ cleanupIntervalId = setInterval(cleanupOldAgents, CLEANUP_INTERVAL_MS);
272
+ console.log(`[AgentManager] Cleanup interval started (every ${CLEANUP_INTERVAL_MS / 60000}min)`);
215
273
  }
216
274
  /**
217
275
  * 정기 cleanup 중지
package/dist/ai/claude.js CHANGED
@@ -23,7 +23,7 @@ function getClient() {
23
23
  }
24
24
  return anthropic;
25
25
  }
26
- // Thinking 레벨별 설정 (비율 및 최대값)
26
+ // Thinking 레벨별 설정
27
27
  export const THINKING_CONFIGS = {
28
28
  off: { ratio: 0, maxBudget: 0 },
29
29
  low: { ratio: 0.3, maxBudget: 5000 },
@@ -36,7 +36,7 @@ export const MODELS = {
36
36
  id: "claude-haiku-3-5-20241022",
37
37
  name: "Claude Haiku 3.5",
38
38
  contextWindow: 200000,
39
- supportsThinking: false, // Haiku는 thinking 미지원
39
+ supportsThinking: false,
40
40
  },
41
41
  sonnet: {
42
42
  id: "claude-sonnet-4-20250514",
@@ -51,59 +51,47 @@ export const MODELS = {
51
51
  supportsThinking: true,
52
52
  },
53
53
  };
54
- // 동적 토큰 계산을 위한 설정
55
- const MIN_OUTPUT_TOKENS = 4096; // 최소 출력 토큰
56
- const OUTPUT_BUFFER_RATIO = 0.3; // 컨텍스트의 30%를 출력용으로 예약
54
+ // 동적 토큰 계산 설정
55
+ const MIN_OUTPUT_TOKENS = 4096;
56
+ const OUTPUT_BUFFER_RATIO = 0.3;
57
57
  /**
58
58
  * 동적으로 max_tokens와 thinking budget 계산
59
- *
60
- * @param modelId 모델 ID
61
- * @param thinkingLevel thinking 레벨
62
- * @param inputTokens 현재 입력 토큰 수 (시스템 프롬프트 + 히스토리)
63
- * @returns { maxTokens, thinkingBudget }
64
59
  */
65
60
  export function calculateTokenBudgets(modelId, thinkingLevel, inputTokens) {
66
61
  const model = MODELS[modelId];
67
62
  const thinkingConfig = THINKING_CONFIGS[thinkingLevel];
68
- // Thinking 미지원 모델이거나 off인 경우
69
63
  if (!model.supportsThinking || thinkingLevel === "off") {
70
- // 간단히 고정 max_tokens 사용
71
64
  return { maxTokens: 8192, thinkingBudget: 0 };
72
65
  }
73
- // 사용 가능한 출력 토큰 계산
74
- // 컨텍스트 윈도우 - 입력 토큰 = 출력 가능 토큰
75
66
  const availableOutputTokens = model.contextWindow - inputTokens;
76
- // 최소 출력 토큰 보장
77
67
  const maxTokens = Math.max(MIN_OUTPUT_TOKENS, Math.floor(availableOutputTokens * OUTPUT_BUFFER_RATIO));
78
- // thinking budget 계산: min(레벨별 최대값, max_tokens * 비율)
79
- // API 조건: max_tokens > budget_tokens 이므로 max_tokens - 1024 로 상한 설정
80
68
  const calculatedBudget = Math.floor(maxTokens * thinkingConfig.ratio);
81
- const thinkingBudget = Math.min(thinkingConfig.maxBudget, calculatedBudget, maxTokens - 1024 // max_tokens > budget_tokens 조건 충족
82
- );
83
- // budget이 1024 미만이면 thinking 비활성화 (의미 없음)
69
+ const thinkingBudget = Math.min(thinkingConfig.maxBudget, calculatedBudget, maxTokens - 1024);
84
70
  if (thinkingBudget < 1024) {
85
71
  return { maxTokens, thinkingBudget: 0 };
86
72
  }
87
73
  return { maxTokens, thinkingBudget };
88
74
  }
89
- export async function chat(messages, systemPrompt, modelId = "sonnet", _thinkingLevel // 사용 안 함 (non-streaming에서 에러 발생)
90
- ) {
75
+ /**
76
+ * Claude API 호출 (스트리밍 내부 사용, thinking 지원)
77
+ * - 스트리밍으로 호출하되 최종 응답만 반환 (사용자에게 중간 메시지 안 보냄)
78
+ * - thinking 활성화 가능
79
+ * - 도구 사용 시에는 non-streaming으로 폴백 (thinking off)
80
+ */
81
+ export async function chat(messages, systemPrompt, modelId = "sonnet", thinkingLevel = "medium") {
91
82
  const client = getClient();
92
83
  const modelConfig = MODELS[modelId];
93
84
  const toolsUsed = [];
94
- // 메시지를 API 형식으로 변환
95
85
  const apiMessages = messages.map((m) => ({
96
86
  role: m.role,
97
87
  content: m.content,
98
88
  }));
99
- // 입력 토큰 추정 (대략적)
89
+ // 입력 토큰 추정
100
90
  const estimateInputTokens = () => {
101
91
  let total = 0;
102
- // 시스템 프롬프트
103
92
  if (systemPrompt) {
104
- total += Math.ceil(systemPrompt.length / 3); // 대략 3자당 1토큰
93
+ total += Math.ceil(systemPrompt.length / 3);
105
94
  }
106
- // 메시지들
107
95
  for (const msg of apiMessages) {
108
96
  const content = typeof msg.content === "string"
109
97
  ? msg.content
@@ -112,37 +100,65 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", _thinking
112
100
  }
113
101
  return total;
114
102
  };
115
- // 토큰 계산 (thinking 비활성화 - non-streaming에서 에러 발생)
116
103
  const inputTokens = estimateInputTokens();
117
- const maxTokens = 8192;
118
- console.log(`[Chat] model=${modelId}, input~${inputTokens}, maxTokens=${maxTokens}`);
119
- // API 요청 파라미터 빌드 (도구 루프에서도 동일하게 사용)
120
- const buildRequestParams = () => {
104
+ const { maxTokens, thinkingBudget } = calculateTokenBudgets(modelId, thinkingLevel, inputTokens);
105
+ console.log(`[Chat] model=${modelId}, thinking=${thinkingLevel}, input~${inputTokens}, maxTokens=${maxTokens}, budget=${thinkingBudget}`);
106
+ // 스트리밍 호출 (thinking 사용 가능)
107
+ const streamRequest = async () => {
121
108
  const params = {
122
109
  model: modelConfig.id,
123
110
  max_tokens: maxTokens,
124
111
  messages: apiMessages,
125
112
  tools: tools,
113
+ stream: true,
114
+ };
115
+ if (systemPrompt) {
116
+ params.system = systemPrompt;
117
+ }
118
+ // thinking 활성화
119
+ if (thinkingBudget > 0) {
120
+ params.thinking = {
121
+ type: "enabled",
122
+ budget_tokens: thinkingBudget,
123
+ };
124
+ }
125
+ // 스트리밍하되 최종 메시지만 반환
126
+ const stream = client.messages.stream(params);
127
+ return await stream.finalMessage();
128
+ };
129
+ // Non-streaming 호출 (도구 사용 루프용, thinking off)
130
+ const nonStreamRequest = async () => {
131
+ const params = {
132
+ model: modelConfig.id,
133
+ max_tokens: 8192,
134
+ messages: apiMessages,
135
+ tools: tools,
126
136
  };
127
137
  if (systemPrompt) {
128
138
  params.system = systemPrompt;
129
139
  }
130
- return params;
140
+ return await client.messages.create(params);
131
141
  };
142
+ // 첫 번째 호출은 스트리밍 (thinking 사용)
132
143
  let response;
133
- response = await withRetry(() => withTimeout(() => client.messages.create(buildRequestParams()), API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
134
- // Tool use 루프 - Claude가 도구 사용을 멈출 때까지 반복
144
+ try {
145
+ response = await withRetry(() => withTimeout(streamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
146
+ }
147
+ catch (error) {
148
+ // 스트리밍 실패 시 non-streaming 폴백
149
+ console.log("[Chat] Streaming failed, falling back to non-streaming");
150
+ response = await withRetry(() => withTimeout(nonStreamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
151
+ }
152
+ // Tool use 루프 (non-streaming, thinking off)
135
153
  let iterations = 0;
136
154
  while (response.stop_reason === "tool_use" && iterations < MAX_TOOL_ITERATIONS) {
137
155
  iterations++;
138
156
  const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
139
- // 도구 병렬 실행 (성능 최적화)
140
157
  console.log(`[Tool] Executing ${toolUseBlocks.length} tool(s) in parallel`);
141
158
  const toolExecutions = await Promise.all(toolUseBlocks.map(async (toolUse) => {
142
159
  const startTime = Date.now();
143
- console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input).slice(0, 200));
160
+ console.log(`[Tool] ${toolUse.name}:`, JSON.stringify(toolUse.input).slice(0, TOOL_INPUT_SUMMARY_LENGTH));
144
161
  try {
145
- // 도구별 타임아웃 적용
146
162
  const timeout = getToolTimeout(toolUse.name);
147
163
  const result = await Promise.race([
148
164
  executeTool(toolUse.name, toolUse.input),
@@ -150,32 +166,17 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", _thinking
150
166
  ]);
151
167
  const elapsed = Date.now() - startTime;
152
168
  console.log(`[Tool] ${toolUse.name} completed in ${elapsed}ms`);
153
- // 스마트 결과 압축
154
169
  const compressedResult = compressToolResult(toolUse.name, result);
155
- return {
156
- toolUse,
157
- result: compressedResult,
158
- success: true,
159
- };
170
+ return { toolUse, result: compressedResult, success: true };
160
171
  }
161
172
  catch (error) {
162
173
  const elapsed = Date.now() - startTime;
163
174
  const errorMsg = error instanceof Error ? error.message : String(error);
164
175
  console.error(`[Tool] ${toolUse.name} failed after ${elapsed}ms:`, errorMsg);
165
- return {
166
- toolUse,
167
- result: `Error: ${errorMsg}`,
168
- success: false,
169
- };
176
+ return { toolUse, result: `Error: ${errorMsg}`, success: false };
170
177
  }
171
178
  }));
172
- // 결과 수집
173
- const toolResults = toolExecutions.map((exec) => ({
174
- type: "tool_result",
175
- tool_use_id: exec.toolUse.id,
176
- content: exec.result,
177
- }));
178
- // 도구 사용 기록
179
+ // 도구 결과 기록
179
180
  for (const exec of toolExecutions) {
180
181
  toolsUsed.push({
181
182
  name: exec.toolUse.name,
@@ -183,34 +184,30 @@ export async function chat(messages, systemPrompt, modelId = "sonnet", _thinking
183
184
  output: exec.result.slice(0, TOOL_OUTPUT_SUMMARY_LENGTH),
184
185
  });
185
186
  }
186
- // 어시스턴트 메시지와 도구 결과 추가
187
+ // 어시스턴트 메시지 추가 (도구 호출)
187
188
  apiMessages.push({
188
189
  role: "assistant",
189
190
  content: response.content,
190
191
  });
192
+ // 도구 결과 메시지 추가
191
193
  apiMessages.push({
192
194
  role: "user",
193
- content: toolResults,
195
+ content: toolExecutions.map((exec) => ({
196
+ type: "tool_result",
197
+ tool_use_id: exec.toolUse.id,
198
+ content: exec.result,
199
+ })),
194
200
  });
195
- // 다음 응답 요청 (도구 루프에서도 thinking 유지)
196
- response = await withRetry(() => withTimeout(() => client.messages.create(buildRequestParams()), API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
197
- }
198
- // 반복 횟수 초과 시 경고
199
- if (iterations >= MAX_TOOL_ITERATIONS) {
200
- console.warn(`[Warning] Tool use loop reached max iterations (${MAX_TOOL_ITERATIONS})`);
201
- return { text: "도구 실행이 너무 많이 반복됐어. 다시 시도해줄래?", toolsUsed };
201
+ // 다음 API 호출 (non-streaming, thinking off - 도구 결과 처리)
202
+ response = await withRetry(() => withTimeout(nonStreamRequest, API_TIMEOUT_MS, "API 응답 시간 초과"), API_RETRY_OPTIONS);
202
203
  }
203
- // 최종 텍스트 응답 추출
204
- const textBlock = response.content.find((block) => block.type === "text");
205
- return {
206
- text: textBlock?.text ?? "응답을 생성하지 못했어. 다시 시도해줄래?",
207
- toolsUsed
208
- };
204
+ // 최종 텍스트 추출
205
+ const textBlocks = response.content.filter((block) => block.type === "text");
206
+ const text = textBlocks.map((b) => b.text).join("\n");
207
+ return { text, toolsUsed };
209
208
  }
210
209
  /**
211
- * 스마트 채팅 - chat()의 단순 래퍼
212
- *
213
- * 도구 사용 여부를 별도로 반환하여 호출자가 구분할 수 있게 함
210
+ * chat()의 간단한 래퍼 - 도구 사용 여부 반환
214
211
  */
215
212
  export async function chatSmart(messages, systemPrompt, modelId, thinkingLevel = "medium") {
216
213
  const result = await chat(messages, systemPrompt, modelId, thinkingLevel);
@@ -72,9 +72,11 @@ export function createBot(token) {
72
72
  // 명령어 목록 등록
73
73
  bot.api
74
74
  .setMyCommands([
75
+ { command: "help", description: "도움말 보기" },
76
+ { command: "model", description: "AI 모델 변경" },
75
77
  { command: "compact", description: "대화 정리하기" },
76
78
  { command: "memory", description: "최근 기억 보기" },
77
- { command: "reminders", description: "알림 목록 보기" },
79
+ { command: "health", description: " 상태 확인" },
78
80
  ])
79
81
  .catch((err) => console.error("Failed to set commands:", err));
80
82
  // 핸들러 등록
@@ -60,6 +60,32 @@ import { setHeartbeatConfig, getHeartbeatConfig, disableHeartbeat, } from "../..
60
60
  import { getWorkspace, invalidateWorkspaceCache, buildSystemPrompt, extractName, } from "../utils/index.js";
61
61
  import { ensureDefaultCronJobs } from "../../cron/scheduler.js";
62
62
  export function registerCommands(bot) {
63
+ // /help 명령어 - 전체 기능 안내
64
+ bot.command("help", async (ctx) => {
65
+ await ctx.reply(`📖 도움말\n\n` +
66
+ `🎯 기본 기능\n` +
67
+ `/model - AI 모델 변경 (sonnet/opus/haiku)\n` +
68
+ `/compact - 대화 압축해서 토큰 절약\n` +
69
+ `/clear - 대화 초기화\n\n` +
70
+ `📌 기억/핀\n` +
71
+ `/memory - 최근 기억 보기\n` +
72
+ `/pin [내용] - 중요한 정보 핀하기\n` +
73
+ `/pins - 핀 목록 보기\n` +
74
+ `/context - 현재 맥락 상태\n\n` +
75
+ `⏰ 알림/일정\n` +
76
+ `/reminders - 알림 목록\n` +
77
+ `/briefing - 일일 브리핑 켜기/상태\n` +
78
+ `/calendar - 오늘 일정 보기\n\n` +
79
+ `⚙️ 설정\n` +
80
+ `/setup - 기능별 설정 관리\n` +
81
+ `/health - 봇 상태 확인\n` +
82
+ `/reset - 페르소나 초기화\n\n` +
83
+ `💡 자연어로도 말할 수 있어요:\n` +
84
+ `• "opus로 바꿔줘"\n` +
85
+ `• "10분 뒤에 알려줘"\n` +
86
+ `• "기억해: 나는 채식주의자야"\n` +
87
+ `• "내일 일정 뭐야?"`);
88
+ });
63
89
  // /start 명령어
64
90
  bot.command("start", async (ctx) => {
65
91
  const chatId = ctx.chat.id;
@@ -210,19 +236,19 @@ export function registerCommands(bot) {
210
236
  const modelList = Object.entries(MODELS)
211
237
  .map(([id, m]) => `${id === currentModel ? "→" : " "} /model ${id} - ${m.name}`)
212
238
  .join("\n");
213
- await ctx.reply(`Current model: ${MODELS[currentModel].name}\n\n` +
214
- `Available models:\n${modelList}\n\n` +
215
- `Tip: You can also ask me to change models in natural language!`);
239
+ await ctx.reply(`현재 모델: ${MODELS[currentModel].name}\n\n` +
240
+ `사용 가능한 모델:\n${modelList}\n\n` +
241
+ `팁: "모델 바꿔줘"처럼 자연어로도 바꿀 있어!`);
216
242
  return;
217
243
  }
218
244
  if (arg in MODELS) {
219
245
  const modelId = arg;
220
246
  setModel(chatId, modelId);
221
- await ctx.reply(`Model changed to: ${MODELS[modelId].name}`);
247
+ await ctx.reply(`모델 변경됨: ${MODELS[modelId].name}`);
222
248
  }
223
249
  else {
224
- await ctx.reply(`Unknown model: ${arg}\n\n` +
225
- `Available: sonnet, opus, haiku`);
250
+ await ctx.reply(`모르는 모델이야: ${arg}\n\n` +
251
+ `사용 가능: sonnet, opus, haiku`);
226
252
  }
227
253
  });
228
254
  // /setup 명령어 - 추가 기능 설정 및 관리
@@ -2,10 +2,7 @@ import { MODELS } from "../../ai/claude.js";
2
2
  import { getWorkspacePath } from "../../workspace/index.js";
3
3
  import { getToolsDescription } from "../../tools/index.js";
4
4
  import { getWorkspace } from "./cache.js";
5
- import { embed } from "../../memory/embeddings.js";
6
- import { search } from "../../memory/vectorStore.js";
7
5
  import { buildContextForPrompt, getCurrentChatId } from "../../session/state.js";
8
- import { SEARCH_CONTEXT_LENGTH, PROMPT_MEMORY_SEARCH_LIMIT, PROMPT_MEMORY_MIN_SCORE, MEMORY_PREVIEW_LENGTH, } from "../../utils/constants.js";
9
6
  import * as os from "os";
10
7
  function getRuntimeInfo(modelId) {
11
8
  const model = MODELS[modelId];
@@ -49,32 +46,6 @@ export function extractName(identityContent) {
49
46
  }
50
47
  return null;
51
48
  }
52
- // ============== 메모리 검색 ==============
53
- function extractSearchContext(history) {
54
- const recent = history.slice(-3);
55
- return recent
56
- .filter((m) => m.role === "user")
57
- .map((m) => (typeof m.content === "string" ? m.content : ""))
58
- .join(" ")
59
- .slice(0, SEARCH_CONTEXT_LENGTH);
60
- }
61
- async function getRelevantMemories(history) {
62
- try {
63
- const context = extractSearchContext(history);
64
- if (!context.trim())
65
- return "";
66
- const queryEmbedding = await embed(context);
67
- const results = await search(queryEmbedding, PROMPT_MEMORY_SEARCH_LIMIT, PROMPT_MEMORY_MIN_SCORE);
68
- if (results.length === 0)
69
- return "";
70
- return results
71
- .map((r) => `- (${r.source}): ${r.text.slice(0, MEMORY_PREVIEW_LENGTH)}${r.text.length > MEMORY_PREVIEW_LENGTH ? "..." : ""}`)
72
- .join("\n");
73
- }
74
- catch {
75
- return "";
76
- }
77
- }
78
49
  // ============== Core Identity 빌더 ==============
79
50
  function buildCoreIdentity(name) {
80
51
  const displayName = name || "AI companion";
@@ -98,11 +69,8 @@ You are NOT a generic assistant or chatbot. You are someone developing a real re
98
69
  }
99
70
  // ============== 메인 빌드 함수 ==============
100
71
  export async function buildSystemPrompt(modelId, history) {
101
- // 🚀 병렬 실행: 워크스페이스 로드 + 관련 메모리 검색
102
- const [workspace, relevantMemoriesResult] = await Promise.all([
103
- getWorkspace(),
104
- history && history.length > 0 ? getRelevantMemories(history) : Promise.resolve(""),
105
- ]);
72
+ // 워크스페이스 로드 (메모리 검색은 memory_search 도구로 필요시에만)
73
+ const workspace = await getWorkspace();
106
74
  const runtime = getRuntimeInfo(modelId);
107
75
  const dateTime = getKoreanDateTime();
108
76
  const parts = [];
@@ -174,15 +142,7 @@ export async function buildSystemPrompt(modelId, history) {
174
142
  parts.push(workspace.recentDaily);
175
143
  parts.push("");
176
144
  }
177
- // 관련 기억 (벡터 검색) - 위에서 병렬로 미리 가져옴
178
- if (relevantMemoriesResult) {
179
- parts.push("# Relevant Memories");
180
- parts.push("");
181
- parts.push("Related information from older records:");
182
- parts.push("");
183
- parts.push(relevantMemoriesResult);
184
- parts.push("");
185
- }
145
+ // 관련 기억: memory_search 도구로 필요시에만 검색 (자동 검색 제거됨)
186
146
  // 장기 기억
187
147
  if (workspace.memory) {
188
148
  parts.push("# Long-term Memory");
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Sub-agent tools
3
+ */
4
+ import { getCurrentChatId } from "../session/state.js";
5
+ import { spawnAgent, listAgents, cancelAgent, } from "../agents/index.js";
6
+ // spawn_agent
7
+ export async function executeSpawnAgent(input) {
8
+ const chatId = getCurrentChatId();
9
+ if (!chatId) {
10
+ return "Error: No active chat session";
11
+ }
12
+ const task = input.task;
13
+ if (!task || task.trim().length === 0) {
14
+ return "Error: Task description is required";
15
+ }
16
+ const agentId = await spawnAgent(task, chatId);
17
+ return `Sub-agent spawned! 🤖\nID: ${agentId}\nTask: ${task.slice(0, 100)}${task.length > 100 ? "..." : ""}\n\nThe agent is working in the background. Results will be sent to this chat when complete.`;
18
+ }
19
+ // list_agents
20
+ export function executeListAgents() {
21
+ const chatId = getCurrentChatId();
22
+ const agents = listAgents(chatId || undefined);
23
+ if (agents.length === 0) {
24
+ return "No sub-agents found.";
25
+ }
26
+ const lines = agents.map((a) => {
27
+ const status = {
28
+ running: "🔄 Running",
29
+ completed: "✅ Completed",
30
+ failed: "❌ Failed",
31
+ cancelled: "⏹️ Cancelled",
32
+ }[a.status];
33
+ const time = a.completedAt
34
+ ? `(${Math.round((a.completedAt.getTime() - a.createdAt.getTime()) / 1000)}s)`
35
+ : "";
36
+ return `${a.id}: ${status} ${time}\n Task: ${a.task.slice(0, 60)}${a.task.length > 60 ? "..." : ""}`;
37
+ });
38
+ return `Sub-agents:\n${lines.join("\n\n")}`;
39
+ }
40
+ // cancel_agent
41
+ export function executeCancelAgent(input) {
42
+ const agentId = input.agent_id;
43
+ if (!agentId) {
44
+ return "Error: Agent ID is required";
45
+ }
46
+ const success = cancelAgent(agentId);
47
+ if (success) {
48
+ return `Sub-agent ${agentId} cancelled.`;
49
+ }
50
+ else {
51
+ return `Could not cancel agent ${agentId}. It may not exist or already completed.`;
52
+ }
53
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * File operation tools
3
+ */
4
+ import * as fs from "fs/promises";
5
+ import * as path from "path";
6
+ import { isPathAllowed } from "./pathCheck.js";
7
+ // read_file
8
+ export async function executeReadFile(input) {
9
+ const filePath = input.path;
10
+ if (!isPathAllowed(filePath)) {
11
+ return `Error: Access denied. Path not in allowed directories.`;
12
+ }
13
+ const content = await fs.readFile(filePath, "utf-8");
14
+ return content;
15
+ }
16
+ // write_file
17
+ export async function executeWriteFile(input) {
18
+ const filePath = input.path;
19
+ const content = input.content;
20
+ if (!isPathAllowed(filePath)) {
21
+ return `Error: Access denied. Path not in allowed directories.`;
22
+ }
23
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
24
+ await fs.writeFile(filePath, content, "utf-8");
25
+ return `File written successfully: ${filePath}`;
26
+ }
27
+ // edit_file
28
+ export async function executeEditFile(input) {
29
+ const filePath = input.path;
30
+ const oldText = input.oldText;
31
+ const newText = input.newText;
32
+ if (!isPathAllowed(filePath)) {
33
+ return `Error: Access denied. Path not in allowed directories.`;
34
+ }
35
+ // 파일 읽기
36
+ let content;
37
+ try {
38
+ content = await fs.readFile(filePath, "utf-8");
39
+ }
40
+ catch (error) {
41
+ return `Error: Could not read file "${filePath}". ${error instanceof Error ? error.message : String(error)}`;
42
+ }
43
+ // oldText 찾기
44
+ const index = content.indexOf(oldText);
45
+ if (index === -1) {
46
+ return `Error: oldText not found in file. Make sure the text matches exactly (including whitespace).`;
47
+ }
48
+ // 첫 번째만 교체
49
+ const newContent = content.slice(0, index) + newText + content.slice(index + oldText.length);
50
+ // 저장
51
+ await fs.writeFile(filePath, newContent, "utf-8");
52
+ return `File edited successfully: ${filePath}`;
53
+ }
54
+ // list_directory
55
+ export async function executeListDirectory(input) {
56
+ const dirPath = input.path;
57
+ if (!isPathAllowed(dirPath)) {
58
+ return `Error: Access denied. Path not in allowed directories.`;
59
+ }
60
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
61
+ const list = entries.map((e) => `${e.isDirectory() ? "📁" : "📄"} ${e.name}`);
62
+ return list.join("\n");
63
+ }