companionbot 0.1.1 → 0.2.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/agents/index.js +4 -0
- package/dist/agents/manager.js +171 -0
- package/dist/agents/types.js +4 -0
- package/dist/telegram/bot.js +3 -0
- package/dist/tools/index.js +327 -11
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentManager - Sub-agent 생성 및 관리
|
|
3
|
+
*
|
|
4
|
+
* 각 sub-agent는:
|
|
5
|
+
* - 별도의 Claude API 호출로 독립 실행
|
|
6
|
+
* - 메인 conversation과 별개의 context
|
|
7
|
+
* - 비동기로 실행, 완료 시 callback
|
|
8
|
+
*/
|
|
9
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
// Agent 저장소
|
|
12
|
+
const agents = new Map();
|
|
13
|
+
// Bot 인스턴스 (결과 전송용)
|
|
14
|
+
let botInstance = null;
|
|
15
|
+
// Anthropic 클라이언트
|
|
16
|
+
let anthropic = null;
|
|
17
|
+
function getClient() {
|
|
18
|
+
if (!anthropic) {
|
|
19
|
+
anthropic = new Anthropic();
|
|
20
|
+
}
|
|
21
|
+
return anthropic;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Bot 인스턴스 설정 (시작 시 호출)
|
|
25
|
+
*/
|
|
26
|
+
export function setAgentBot(bot) {
|
|
27
|
+
botInstance = bot;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sub-agent 생성 및 실행
|
|
31
|
+
*/
|
|
32
|
+
export async function spawnAgent(task, chatId) {
|
|
33
|
+
const id = randomUUID().slice(0, 8);
|
|
34
|
+
const agent = {
|
|
35
|
+
id,
|
|
36
|
+
task,
|
|
37
|
+
status: "running",
|
|
38
|
+
chatId,
|
|
39
|
+
createdAt: new Date(),
|
|
40
|
+
};
|
|
41
|
+
agents.set(id, agent);
|
|
42
|
+
// 비동기로 agent 실행 (await 하지 않음)
|
|
43
|
+
runAgent(agent).catch((err) => {
|
|
44
|
+
console.error(`[Agent ${id}] Error:`, err);
|
|
45
|
+
});
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Agent 실행 (내부 함수)
|
|
50
|
+
*/
|
|
51
|
+
async function runAgent(agent) {
|
|
52
|
+
const client = getClient();
|
|
53
|
+
const systemPrompt = `You are a sub-agent assistant. Your job is to complete a specific task and report the result concisely.
|
|
54
|
+
|
|
55
|
+
TASK: ${agent.task}
|
|
56
|
+
|
|
57
|
+
Guidelines:
|
|
58
|
+
- Focus only on the given task
|
|
59
|
+
- Be concise but thorough
|
|
60
|
+
- Report results clearly
|
|
61
|
+
- If you cannot complete the task, explain why
|
|
62
|
+
|
|
63
|
+
Complete the task and provide your final answer.`;
|
|
64
|
+
try {
|
|
65
|
+
console.log(`[Agent ${agent.id}] Starting: ${agent.task.slice(0, 50)}...`);
|
|
66
|
+
const response = await client.messages.create({
|
|
67
|
+
model: "claude-sonnet-4-20250514",
|
|
68
|
+
max_tokens: 2048,
|
|
69
|
+
system: systemPrompt,
|
|
70
|
+
messages: [
|
|
71
|
+
{
|
|
72
|
+
role: "user",
|
|
73
|
+
content: `Please complete this task: ${agent.task}`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
// 결과 추출
|
|
78
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
79
|
+
const result = textBlock?.text ?? "No response generated.";
|
|
80
|
+
// Agent 상태 업데이트
|
|
81
|
+
agent.status = "completed";
|
|
82
|
+
agent.completedAt = new Date();
|
|
83
|
+
agent.result = result;
|
|
84
|
+
console.log(`[Agent ${agent.id}] Completed`);
|
|
85
|
+
// 결과를 원래 chat에 전송
|
|
86
|
+
await sendAgentResult(agent);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
agent.status = "failed";
|
|
90
|
+
agent.completedAt = new Date();
|
|
91
|
+
agent.error = error instanceof Error ? error.message : String(error);
|
|
92
|
+
console.error(`[Agent ${agent.id}] Failed:`, agent.error);
|
|
93
|
+
// 실패도 알림
|
|
94
|
+
await sendAgentResult(agent);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Agent 결과를 chat에 전송
|
|
99
|
+
*/
|
|
100
|
+
async function sendAgentResult(agent) {
|
|
101
|
+
if (!botInstance) {
|
|
102
|
+
console.warn("[Agent] No bot instance, cannot send result");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let message;
|
|
106
|
+
if (agent.status === "completed") {
|
|
107
|
+
message = `🤖 **Sub-agent 완료** (${agent.id})\n\n📋 Task: ${agent.task.slice(0, 100)}${agent.task.length > 100 ? "..." : ""}\n\n✅ Result:\n${agent.result}`;
|
|
108
|
+
}
|
|
109
|
+
else if (agent.status === "failed") {
|
|
110
|
+
message = `🤖 **Sub-agent 실패** (${agent.id})\n\n📋 Task: ${agent.task.slice(0, 100)}${agent.task.length > 100 ? "..." : ""}\n\n❌ Error: ${agent.error}`;
|
|
111
|
+
}
|
|
112
|
+
else if (agent.status === "cancelled") {
|
|
113
|
+
message = `🤖 **Sub-agent 취소됨** (${agent.id})`;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
return; // running 상태면 전송 안 함
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await botInstance.api.sendMessage(agent.chatId, message);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.error(`[Agent ${agent.id}] Failed to send result:`, err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Agent 목록 조회
|
|
127
|
+
*/
|
|
128
|
+
export function listAgents(chatId) {
|
|
129
|
+
const allAgents = Array.from(agents.values());
|
|
130
|
+
if (chatId !== undefined) {
|
|
131
|
+
return allAgents.filter((a) => a.chatId === chatId);
|
|
132
|
+
}
|
|
133
|
+
return allAgents;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Agent 취소
|
|
137
|
+
*/
|
|
138
|
+
export function cancelAgent(agentId) {
|
|
139
|
+
const agent = agents.get(agentId);
|
|
140
|
+
if (!agent) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (agent.status !== "running") {
|
|
144
|
+
return false; // 이미 완료된 agent는 취소 불가
|
|
145
|
+
}
|
|
146
|
+
// 실제로 실행 중인 API 호출을 취소할 수는 없지만
|
|
147
|
+
// 상태를 cancelled로 표시하고 결과 전송 시 무시되도록 함
|
|
148
|
+
agent.status = "cancelled";
|
|
149
|
+
agent.completedAt = new Date();
|
|
150
|
+
console.log(`[Agent ${agentId}] Cancelled`);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Agent 상태 조회
|
|
155
|
+
*/
|
|
156
|
+
export function getAgent(agentId) {
|
|
157
|
+
return agents.get(agentId);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 오래된 agent 정리 (1시간 이상)
|
|
161
|
+
*/
|
|
162
|
+
export function cleanupOldAgents() {
|
|
163
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
164
|
+
for (const [id, agent] of agents.entries()) {
|
|
165
|
+
if (agent.completedAt && agent.completedAt.getTime() < oneHourAgo) {
|
|
166
|
+
agents.delete(id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// 10분마다 정리
|
|
171
|
+
setInterval(cleanupOldAgents, 10 * 60 * 1000);
|
package/dist/telegram/bot.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Bot } from "grammy";
|
|
|
2
2
|
import { setBotInstance, restoreReminders } from "../reminders/index.js";
|
|
3
3
|
import { setBriefingBot, restoreBriefings } from "../briefing/index.js";
|
|
4
4
|
import { setHeartbeatBot, restoreHeartbeats } from "../heartbeat/index.js";
|
|
5
|
+
import { setAgentBot } from "../agents/index.js";
|
|
5
6
|
import { registerCommands, registerMessageHandlers } from "./handlers/index.js";
|
|
6
7
|
// Re-export for external use
|
|
7
8
|
export { invalidateWorkspaceCache } from "./utils/index.js";
|
|
@@ -19,6 +20,8 @@ export function createBot(token) {
|
|
|
19
20
|
// Heartbeat 초기화
|
|
20
21
|
setHeartbeatBot(bot);
|
|
21
22
|
restoreHeartbeats().catch((err) => console.error("Failed to restore heartbeats:", err));
|
|
23
|
+
// Sub-agent 시스템 초기화
|
|
24
|
+
setAgentBot(bot);
|
|
22
25
|
// 에러 핸들링
|
|
23
26
|
bot.catch((err) => {
|
|
24
27
|
console.error("Bot error:", err);
|
package/dist/tools/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as fsSync from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { exec } from "child_process";
|
|
4
|
+
import { exec, spawn } from "child_process";
|
|
5
5
|
import { promisify } from "util";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
6
7
|
import { MODELS } from "../ai/claude.js";
|
|
7
8
|
import { getCurrentChatId, setModel, getModel } from "../session/state.js";
|
|
8
9
|
import { getWorkspacePath, saveWorkspaceFile, appendToMemory, deleteBootstrap, } from "../workspace/index.js";
|
|
@@ -11,7 +12,20 @@ import { createReminder, deleteReminder, getReminders, parseTimeExpression, } fr
|
|
|
11
12
|
import { isCalendarConfigured, getEvents, addEvent, deleteEvent, formatEvent, parseDateExpression, } from "../calendar/index.js";
|
|
12
13
|
import { setHeartbeatConfig, getHeartbeatConfig, disableHeartbeat, runHeartbeatNow, } from "../heartbeat/index.js";
|
|
13
14
|
import { setBriefingConfig, getBriefingConfig, disableBriefing, sendBriefingNow, } from "../briefing/index.js";
|
|
15
|
+
import { spawnAgent, listAgents, cancelAgent, } from "../agents/index.js";
|
|
14
16
|
const execAsync = promisify(exec);
|
|
17
|
+
// 메모리에 세션 저장
|
|
18
|
+
const sessions = new Map();
|
|
19
|
+
// Output buffer 최대 크기 (라인 수)
|
|
20
|
+
const MAX_OUTPUT_LINES = 1000;
|
|
21
|
+
function appendOutput(session, data) {
|
|
22
|
+
const lines = data.split("\n");
|
|
23
|
+
session.outputBuffer.push(...lines);
|
|
24
|
+
// 버퍼 크기 제한
|
|
25
|
+
if (session.outputBuffer.length > MAX_OUTPUT_LINES) {
|
|
26
|
+
session.outputBuffer = session.outputBuffer.slice(-MAX_OUTPUT_LINES);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
// 홈 디렉토리
|
|
16
30
|
const home = process.env.HOME || "";
|
|
17
31
|
// 허용된 디렉토리 (보안을 위해 제한)
|
|
@@ -118,7 +132,13 @@ export const tools = [
|
|
|
118
132
|
},
|
|
119
133
|
{
|
|
120
134
|
name: "run_command",
|
|
121
|
-
description:
|
|
135
|
+
description: `Run a shell command. Use with caution. Only for safe commands like git status, npm run, etc.
|
|
136
|
+
|
|
137
|
+
When background=true:
|
|
138
|
+
- Command runs in detached mode
|
|
139
|
+
- Returns a session ID immediately
|
|
140
|
+
- Use list_sessions, get_session_log, kill_session to manage
|
|
141
|
+
- Useful for long-running commands (npm run dev, servers, etc.)`,
|
|
122
142
|
input_schema: {
|
|
123
143
|
type: "object",
|
|
124
144
|
properties: {
|
|
@@ -130,10 +150,70 @@ export const tools = [
|
|
|
130
150
|
type: "string",
|
|
131
151
|
description: "The working directory to run the command in (optional)",
|
|
132
152
|
},
|
|
153
|
+
background: {
|
|
154
|
+
type: "boolean",
|
|
155
|
+
description: "Run in background and return session ID (default: false)",
|
|
156
|
+
},
|
|
157
|
+
timeout: {
|
|
158
|
+
type: "number",
|
|
159
|
+
description: "Timeout in seconds for foreground commands (default: 30)",
|
|
160
|
+
},
|
|
133
161
|
},
|
|
134
162
|
required: ["command"],
|
|
135
163
|
},
|
|
136
164
|
},
|
|
165
|
+
{
|
|
166
|
+
name: "list_sessions",
|
|
167
|
+
description: "List all background command sessions. Shows running and recently completed sessions.",
|
|
168
|
+
input_schema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
status: {
|
|
172
|
+
type: "string",
|
|
173
|
+
enum: ["all", "running", "completed"],
|
|
174
|
+
description: "Filter by status (default: all)",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: [],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "get_session_log",
|
|
182
|
+
description: "Get the output log of a background session.",
|
|
183
|
+
input_schema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
session_id: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "The session ID to get logs from",
|
|
189
|
+
},
|
|
190
|
+
tail: {
|
|
191
|
+
type: "number",
|
|
192
|
+
description: "Number of lines from the end (default: 50)",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ["session_id"],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "kill_session",
|
|
200
|
+
description: "Kill a running background session.",
|
|
201
|
+
input_schema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
session_id: {
|
|
205
|
+
type: "string",
|
|
206
|
+
description: "The session ID to kill",
|
|
207
|
+
},
|
|
208
|
+
signal: {
|
|
209
|
+
type: "string",
|
|
210
|
+
enum: ["SIGTERM", "SIGKILL", "SIGINT"],
|
|
211
|
+
description: "Signal to send (default: SIGTERM)",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
required: ["session_id"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
137
217
|
{
|
|
138
218
|
name: "change_model",
|
|
139
219
|
description: `Change the AI model for this conversation. Use this when the user asks to switch models, or when you determine a different model would be better suited for the task.
|
|
@@ -399,6 +479,56 @@ Use this when the user says things like:
|
|
|
399
479
|
required: [],
|
|
400
480
|
},
|
|
401
481
|
},
|
|
482
|
+
// ============== Sub-Agent 도구 ==============
|
|
483
|
+
{
|
|
484
|
+
name: "spawn_agent",
|
|
485
|
+
description: `Create a sub-agent to handle a complex or time-consuming task independently.
|
|
486
|
+
|
|
487
|
+
The sub-agent will:
|
|
488
|
+
- Run in the background with its own Claude API context
|
|
489
|
+
- Complete the task independently
|
|
490
|
+
- Report results back to this chat when done
|
|
491
|
+
|
|
492
|
+
Use this for:
|
|
493
|
+
- Tasks that require deep focus or analysis
|
|
494
|
+
- Long-running research or summarization
|
|
495
|
+
- Work that can be done in parallel while you handle other things
|
|
496
|
+
|
|
497
|
+
Example: "서브에이전트한테 이 코드 분석 시켜줘"`,
|
|
498
|
+
input_schema: {
|
|
499
|
+
type: "object",
|
|
500
|
+
properties: {
|
|
501
|
+
task: {
|
|
502
|
+
type: "string",
|
|
503
|
+
description: "Detailed description of the task for the sub-agent",
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
required: ["task"],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "list_agents",
|
|
511
|
+
description: "List all sub-agents and their status (running, completed, failed, cancelled).",
|
|
512
|
+
input_schema: {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {},
|
|
515
|
+
required: [],
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: "cancel_agent",
|
|
520
|
+
description: "Cancel a running sub-agent by its ID.",
|
|
521
|
+
input_schema: {
|
|
522
|
+
type: "object",
|
|
523
|
+
properties: {
|
|
524
|
+
agent_id: {
|
|
525
|
+
type: "string",
|
|
526
|
+
description: "The sub-agent ID to cancel",
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
required: ["agent_id"],
|
|
530
|
+
},
|
|
531
|
+
},
|
|
402
532
|
];
|
|
403
533
|
// Tool 실행 함수
|
|
404
534
|
export async function executeTool(name, input) {
|
|
@@ -434,6 +564,8 @@ export async function executeTool(name, input) {
|
|
|
434
564
|
case "run_command": {
|
|
435
565
|
const command = input.command;
|
|
436
566
|
const cwd = input.cwd || path.join(home, "Documents");
|
|
567
|
+
const background = input.background || false;
|
|
568
|
+
const timeout = (input.timeout || 30) * 1000;
|
|
437
569
|
// 화이트리스트 방식: 허용된 명령어만 실행
|
|
438
570
|
const ALLOWED_COMMANDS = [
|
|
439
571
|
"git", "npm", "npx", "node", "ls", "pwd", "cat", "head", "tail",
|
|
@@ -455,18 +587,66 @@ export async function executeTool(name, input) {
|
|
|
455
587
|
if (dangerousArgs.some(arg => parts.includes(arg))) {
|
|
456
588
|
return `Error: Dangerous argument detected.`;
|
|
457
589
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
590
|
+
// 환경 변수는 필요한 것만 화이트리스트로 전달 (민감 정보 노출 방지)
|
|
591
|
+
const safeEnv = {
|
|
592
|
+
PATH: process.env.PATH || "",
|
|
593
|
+
HOME: process.env.HOME || "",
|
|
594
|
+
USER: process.env.USER || "",
|
|
595
|
+
LANG: process.env.LANG || "en_US.UTF-8",
|
|
596
|
+
TERM: process.env.TERM || "xterm",
|
|
597
|
+
};
|
|
598
|
+
// Background 실행
|
|
599
|
+
if (background) {
|
|
600
|
+
const sessionId = randomUUID().slice(0, 8);
|
|
601
|
+
const child = spawn("sh", ["-c", command], {
|
|
602
|
+
cwd,
|
|
603
|
+
env: safeEnv,
|
|
604
|
+
detached: true,
|
|
605
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
606
|
+
});
|
|
607
|
+
const session = {
|
|
608
|
+
id: sessionId,
|
|
609
|
+
pid: child.pid,
|
|
610
|
+
command,
|
|
611
|
+
cwd,
|
|
612
|
+
startTime: new Date(),
|
|
613
|
+
outputBuffer: [],
|
|
614
|
+
process: child,
|
|
615
|
+
status: "running",
|
|
466
616
|
};
|
|
617
|
+
// stdout/stderr 캡처
|
|
618
|
+
child.stdout?.on("data", (data) => {
|
|
619
|
+
appendOutput(session, data.toString());
|
|
620
|
+
});
|
|
621
|
+
child.stderr?.on("data", (data) => {
|
|
622
|
+
appendOutput(session, `[stderr] ${data.toString()}`);
|
|
623
|
+
});
|
|
624
|
+
// 프로세스 종료 핸들링
|
|
625
|
+
child.on("close", (code) => {
|
|
626
|
+
session.endTime = new Date();
|
|
627
|
+
session.exitCode = code;
|
|
628
|
+
session.status = code === 0 ? "completed" : "error";
|
|
629
|
+
});
|
|
630
|
+
child.on("error", (err) => {
|
|
631
|
+
session.status = "error";
|
|
632
|
+
appendOutput(session, `[error] ${err.message}`);
|
|
633
|
+
});
|
|
634
|
+
// unref로 부모 프로세스와 분리
|
|
635
|
+
child.unref();
|
|
636
|
+
sessions.set(sessionId, session);
|
|
637
|
+
return `Background session started.
|
|
638
|
+
Session ID: ${sessionId}
|
|
639
|
+
PID: ${child.pid}
|
|
640
|
+
Command: ${command}
|
|
641
|
+
CWD: ${cwd}
|
|
642
|
+
|
|
643
|
+
Use list_sessions to see all sessions, get_session_log to view output, kill_session to terminate.`;
|
|
644
|
+
}
|
|
645
|
+
// Foreground 실행 (기존 방식)
|
|
646
|
+
try {
|
|
467
647
|
const { stdout, stderr } = await execAsync(command, {
|
|
468
648
|
cwd,
|
|
469
|
-
timeout
|
|
649
|
+
timeout,
|
|
470
650
|
env: safeEnv,
|
|
471
651
|
});
|
|
472
652
|
return stdout || stderr || "Command executed (no output)";
|
|
@@ -475,6 +655,87 @@ export async function executeTool(name, input) {
|
|
|
475
655
|
return `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
476
656
|
}
|
|
477
657
|
}
|
|
658
|
+
case "list_sessions": {
|
|
659
|
+
const statusFilter = input.status || "all";
|
|
660
|
+
const sessionList = [];
|
|
661
|
+
for (const [id, session] of sessions) {
|
|
662
|
+
// 상태 필터링
|
|
663
|
+
if (statusFilter !== "all") {
|
|
664
|
+
if (statusFilter === "running" && session.status !== "running")
|
|
665
|
+
continue;
|
|
666
|
+
if (statusFilter === "completed" && session.status === "running")
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const runtime = session.endTime
|
|
670
|
+
? `${Math.round((session.endTime.getTime() - session.startTime.getTime()) / 1000)}s`
|
|
671
|
+
: `${Math.round((Date.now() - session.startTime.getTime()) / 1000)}s (running)`;
|
|
672
|
+
const status = session.status === "running"
|
|
673
|
+
? "🟢 running"
|
|
674
|
+
: session.status === "completed"
|
|
675
|
+
? "✅ completed"
|
|
676
|
+
: session.status === "killed"
|
|
677
|
+
? "🔴 killed"
|
|
678
|
+
: "❌ error";
|
|
679
|
+
sessionList.push(`[${id}] ${status}
|
|
680
|
+
Command: ${session.command}
|
|
681
|
+
PID: ${session.pid}
|
|
682
|
+
Runtime: ${runtime}
|
|
683
|
+
Exit code: ${session.exitCode ?? "N/A"}`);
|
|
684
|
+
}
|
|
685
|
+
if (sessionList.length === 0) {
|
|
686
|
+
return `No sessions found${statusFilter !== "all" ? ` with status "${statusFilter}"` : ""}.`;
|
|
687
|
+
}
|
|
688
|
+
return `Sessions (${sessionList.length}):\n\n${sessionList.join("\n\n")}`;
|
|
689
|
+
}
|
|
690
|
+
case "get_session_log": {
|
|
691
|
+
const sessionId = input.session_id;
|
|
692
|
+
const tail = input.tail || 50;
|
|
693
|
+
const session = sessions.get(sessionId);
|
|
694
|
+
if (!session) {
|
|
695
|
+
return `Error: Session "${sessionId}" not found. Use list_sessions to see available sessions.`;
|
|
696
|
+
}
|
|
697
|
+
const lines = session.outputBuffer.slice(-tail);
|
|
698
|
+
if (lines.length === 0) {
|
|
699
|
+
return `Session ${sessionId} has no output yet.
|
|
700
|
+
Status: ${session.status}
|
|
701
|
+
Command: ${session.command}`;
|
|
702
|
+
}
|
|
703
|
+
const header = `Session: ${sessionId} (${session.status})
|
|
704
|
+
Command: ${session.command}
|
|
705
|
+
Showing last ${lines.length} lines:
|
|
706
|
+
${"─".repeat(40)}`;
|
|
707
|
+
return `${header}\n${lines.join("\n")}`;
|
|
708
|
+
}
|
|
709
|
+
case "kill_session": {
|
|
710
|
+
const sessionId = input.session_id;
|
|
711
|
+
const signal = input.signal || "SIGTERM";
|
|
712
|
+
const session = sessions.get(sessionId);
|
|
713
|
+
if (!session) {
|
|
714
|
+
return `Error: Session "${sessionId}" not found.`;
|
|
715
|
+
}
|
|
716
|
+
if (session.status !== "running") {
|
|
717
|
+
return `Session ${sessionId} is not running (status: ${session.status}).`;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
// Process group kill (negative PID)
|
|
721
|
+
process.kill(-session.pid, signal);
|
|
722
|
+
session.status = "killed";
|
|
723
|
+
session.endTime = new Date();
|
|
724
|
+
return `Session ${sessionId} (PID ${session.pid}) killed with ${signal}.`;
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
// 단일 프로세스 kill 시도
|
|
728
|
+
try {
|
|
729
|
+
session.process.kill(signal);
|
|
730
|
+
session.status = "killed";
|
|
731
|
+
session.endTime = new Date();
|
|
732
|
+
return `Session ${sessionId} killed with ${signal}.`;
|
|
733
|
+
}
|
|
734
|
+
catch (e) {
|
|
735
|
+
return `Error killing session: ${error instanceof Error ? error.message : String(error)}`;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
478
739
|
case "change_model": {
|
|
479
740
|
const modelId = input.model;
|
|
480
741
|
const reason = input.reason || "";
|
|
@@ -750,6 +1011,52 @@ export async function executeTool(name, input) {
|
|
|
750
1011
|
await sendBriefingNow(chatId);
|
|
751
1012
|
return "Briefing sent!";
|
|
752
1013
|
}
|
|
1014
|
+
// ============== Sub-Agent 도구 ==============
|
|
1015
|
+
case "spawn_agent": {
|
|
1016
|
+
const chatId = getCurrentChatId();
|
|
1017
|
+
if (!chatId) {
|
|
1018
|
+
return "Error: No active chat session";
|
|
1019
|
+
}
|
|
1020
|
+
const task = input.task;
|
|
1021
|
+
if (!task || task.trim().length === 0) {
|
|
1022
|
+
return "Error: Task description is required";
|
|
1023
|
+
}
|
|
1024
|
+
const agentId = await spawnAgent(task, chatId);
|
|
1025
|
+
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.`;
|
|
1026
|
+
}
|
|
1027
|
+
case "list_agents": {
|
|
1028
|
+
const chatId = getCurrentChatId();
|
|
1029
|
+
const agents = listAgents(chatId || undefined);
|
|
1030
|
+
if (agents.length === 0) {
|
|
1031
|
+
return "No sub-agents found.";
|
|
1032
|
+
}
|
|
1033
|
+
const lines = agents.map((a) => {
|
|
1034
|
+
const status = {
|
|
1035
|
+
running: "🔄 Running",
|
|
1036
|
+
completed: "✅ Completed",
|
|
1037
|
+
failed: "❌ Failed",
|
|
1038
|
+
cancelled: "⏹️ Cancelled",
|
|
1039
|
+
}[a.status];
|
|
1040
|
+
const time = a.completedAt
|
|
1041
|
+
? `(${Math.round((a.completedAt.getTime() - a.createdAt.getTime()) / 1000)}s)`
|
|
1042
|
+
: "";
|
|
1043
|
+
return `${a.id}: ${status} ${time}\n Task: ${a.task.slice(0, 60)}${a.task.length > 60 ? "..." : ""}`;
|
|
1044
|
+
});
|
|
1045
|
+
return `Sub-agents:\n${lines.join("\n\n")}`;
|
|
1046
|
+
}
|
|
1047
|
+
case "cancel_agent": {
|
|
1048
|
+
const agentId = input.agent_id;
|
|
1049
|
+
if (!agentId) {
|
|
1050
|
+
return "Error: Agent ID is required";
|
|
1051
|
+
}
|
|
1052
|
+
const success = cancelAgent(agentId);
|
|
1053
|
+
if (success) {
|
|
1054
|
+
return `Sub-agent ${agentId} cancelled.`;
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
return `Could not cancel agent ${agentId}. It may not exist or already completed.`;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
753
1060
|
default:
|
|
754
1061
|
return `Error: Unknown tool: ${name}`;
|
|
755
1062
|
}
|
|
@@ -772,6 +1079,10 @@ export function getToolsDescription(modelId) {
|
|
|
772
1079
|
|
|
773
1080
|
## 시스템
|
|
774
1081
|
- run_command: 셸 명령어 실행 (git, npm 등)
|
|
1082
|
+
- background=true: 백그라운드 실행, 세션 ID 반환
|
|
1083
|
+
- list_sessions: 백그라운드 세션 목록
|
|
1084
|
+
- get_session_log: 세션 출력 로그 조회
|
|
1085
|
+
- kill_session: 세션 종료
|
|
775
1086
|
- change_model: AI 모델 변경
|
|
776
1087
|
- sonnet: 범용 (기본)
|
|
777
1088
|
- opus: 복잡한 작업
|
|
@@ -804,5 +1115,10 @@ export function getToolsDescription(modelId) {
|
|
|
804
1115
|
## 온보딩
|
|
805
1116
|
- save_persona: 페르소나 설정 저장 (온보딩 완료 시)
|
|
806
1117
|
|
|
1118
|
+
## Sub-Agent (백그라운드 작업)
|
|
1119
|
+
- spawn_agent: 복잡한 작업을 sub-agent에게 위임 (독립 실행)
|
|
1120
|
+
- list_agents: 활성 sub-agent 목록
|
|
1121
|
+
- cancel_agent: sub-agent 취소
|
|
1122
|
+
|
|
807
1123
|
허용된 경로: ${path.join(home, "Documents")}, ${path.join(home, "projects")}, 워크스페이스`;
|
|
808
1124
|
}
|