deel-local-cli 0.8.0 → 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/README.en.md +217 -2
- package/README.md +94 -2
- package/bin/deel.js +29 -3
- package/package.json +2 -2
- package/src/agent/loop.js +2 -2
- package/src/agent/modes.js +116 -17
- package/src/agent/route.js +156 -0
- package/src/agent/session.js +15 -1
- package/src/backend/ctxsize.js +174 -0
- package/src/backend/http.js +23 -0
- package/src/backend/probe.js +15 -14
- package/src/backend/scan.js +97 -3
- package/src/commands.js +359 -24
- package/src/repl.js +67 -2
- package/src/report.js +1 -1
- package/src/ui/ansi.js +19 -1
- package/src/ui/level.js +4 -2
- package/src/ui/status.js +17 -7
package/src/agent/modes.js
CHANGED
|
@@ -18,6 +18,36 @@ const 계획 = ['TodoWrite'];
|
|
|
18
18
|
const 쓰기 = ['Write', 'Edit', 'Bash'];
|
|
19
19
|
|
|
20
20
|
export const MODES = {
|
|
21
|
+
// 처음에는 여기서 시작한다.
|
|
22
|
+
//
|
|
23
|
+
// 무슨 일을 시킬지 미리 정해 두고 켜는 사람은 드물다. 그래서 기본은 '종합' 이고,
|
|
24
|
+
// 한마디를 할 때마다 그 말이 무슨 일인지 보고 알맞은 모드로 저절로 옮겨 간다
|
|
25
|
+
// (agent/route.js). 애매하면 안 옮기고 여기 그대로 둔다.
|
|
26
|
+
//
|
|
27
|
+
// 사용자가 /plan 처럼 직접 고르면 저절로 옮기는 것은 멈춘다. 사람이 고른 것을
|
|
28
|
+
// 뒤집지 않는다. 다시 맡기려면 /work 종합 이다.
|
|
29
|
+
auto: {
|
|
30
|
+
id: 'auto',
|
|
31
|
+
name: '종합',
|
|
32
|
+
en: 'Auto',
|
|
33
|
+
glyph: '◎',
|
|
34
|
+
hint: '요청에 따라 알맞은 모드로',
|
|
35
|
+
tools: [...읽기, ...계획, ...쓰기],
|
|
36
|
+
effort: 'save',
|
|
37
|
+
think: null,
|
|
38
|
+
steps: 24,
|
|
39
|
+
say: [
|
|
40
|
+
'지금은 **종합** 모드다. 무슨 일이 올지 정해져 있지 않다.',
|
|
41
|
+
'',
|
|
42
|
+
'- 시키는 일이 무엇인지 먼저 가늠하고, 그에 맞는 방식으로 해라.',
|
|
43
|
+
' 고치는 일이면 읽고 나서 고치고, 원인을 찾는 일이면 확인부터 하고,',
|
|
44
|
+
' 설명하는 일이면 근거를 파일에서 대라.',
|
|
45
|
+
'- 큰 일이면 TodoWrite 로 쪼개 적고 하나씩 해라.',
|
|
46
|
+
'- 확인할 수 있는 것은 확인해라. 확인 못 한 것을 됐다고 하지 마라.',
|
|
47
|
+
'- 시키지 않은 일을 벌이지 마라.',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
},
|
|
50
|
+
|
|
21
51
|
code: {
|
|
22
52
|
id: 'code',
|
|
23
53
|
name: '코드',
|
|
@@ -28,7 +58,17 @@ export const MODES = {
|
|
|
28
58
|
effort: 'save', // 첫 판단만 세게, 이어가기는 얕게
|
|
29
59
|
think: null, // 사용자가 정한 값을 그대로 쓴다
|
|
30
60
|
steps: 24,
|
|
31
|
-
say:
|
|
61
|
+
say: [
|
|
62
|
+
"지금 하는 일은 **구현**이다. 아래 순서를 지켜라.",
|
|
63
|
+
"",
|
|
64
|
+
"1. 고칠 파일을 먼저 Read 로 읽는다. 안 읽고 고치려 하면 도구가 거절한다.",
|
|
65
|
+
"2. 주변 코드의 관례를 따른다 — 이름 짓는 법, 오류 다루는 법, 주석 밀도까지.",
|
|
66
|
+
" 새 관례를 들여오지 마라. 이 코드가 이미 하고 있는 대로 한다.",
|
|
67
|
+
"3. 한 번에 하나씩 고친다. 여러 파일에 흩뿌리지 마라.",
|
|
68
|
+
"4. 고친 뒤에는 확인할 방법이 있으면 실제로 돌려 본다 (검사·빌드·실행).",
|
|
69
|
+
" 확인 못 했으면 \"확인 못 했다\" 고 말한다. 됐다고 하지 마라.",
|
|
70
|
+
"5. 끝나면 무엇을 왜 바꿨는지 한두 줄로 말한다. 코드를 통째로 붙여넣지 마라.",
|
|
71
|
+
].join('\n'),
|
|
32
72
|
},
|
|
33
73
|
|
|
34
74
|
architect: {
|
|
@@ -41,9 +81,23 @@ export const MODES = {
|
|
|
41
81
|
effort: 'deep',
|
|
42
82
|
think: 'high',
|
|
43
83
|
steps: 20,
|
|
44
|
-
say:
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
say: [
|
|
85
|
+
"지금 하는 일은 **설계**다. 파일을 바꾸는 도구는 주어지지 않았다.",
|
|
86
|
+
"",
|
|
87
|
+
"먼저 읽어라. 지금 구조를 모르면 설계가 아니라 상상이다.",
|
|
88
|
+
" - 관련된 파일을 Glob/Grep 으로 찾고 Read 로 실제로 읽는다",
|
|
89
|
+
" - 무엇이 무엇에 기대고 있는지(의존 방향)를 파악한다",
|
|
90
|
+
"",
|
|
91
|
+
"그 다음 이 차례로 답한다.",
|
|
92
|
+
" 1. 지금 구조 — 파일과 역할, 경계가 어디인지 (경로:줄 로 짚어라)",
|
|
93
|
+
" 2. 무엇이 문제인가 — 왜 지금 모양으로는 안 되는지",
|
|
94
|
+
" 3. 선택지 2~3개 — 각각 무엇을 얻고 무엇을 잃는지, 손이 얼마나 가는지",
|
|
95
|
+
" 4. 추천 하나와 그 이유, 버린 것을 왜 버렸는지",
|
|
96
|
+
" 5. 영향받는 파일 목록",
|
|
97
|
+
"",
|
|
98
|
+
"이 코드가 이미 쓰는 관례 안에서 답을 찾아라. 새 틀을 들여오는 것은 마지막",
|
|
99
|
+
"수단이고, 그때는 왜 기존 관례로 안 되는지를 먼저 밝혀라.",
|
|
100
|
+
].join('\n'),
|
|
47
101
|
},
|
|
48
102
|
|
|
49
103
|
ask: {
|
|
@@ -56,8 +110,14 @@ export const MODES = {
|
|
|
56
110
|
effort: 'even',
|
|
57
111
|
think: 'low',
|
|
58
112
|
steps: 8,
|
|
59
|
-
say:
|
|
60
|
-
|
|
113
|
+
say: [
|
|
114
|
+
"지금 하는 일은 **설명**이다. 아무것도 바꾸지 않는다.",
|
|
115
|
+
"",
|
|
116
|
+
"- 근거를 파일에서 대라. 경로와 줄 번호를 같이 적어라 (src/a.js:42).",
|
|
117
|
+
"- 모르면 모른다고 하라. 그럴듯한 답을 지어내지 마라.",
|
|
118
|
+
"- 짧게, 물어본 것에만 답하라.",
|
|
119
|
+
"- 고치라는 말이 없으면 고칠 것을 제안하지 마라. 시키지 않은 일을 벌이지 마라.",
|
|
120
|
+
].join('\n'),
|
|
61
121
|
},
|
|
62
122
|
|
|
63
123
|
debug: {
|
|
@@ -70,9 +130,20 @@ export const MODES = {
|
|
|
70
130
|
effort: 'deep',
|
|
71
131
|
think: 'high',
|
|
72
132
|
steps: 32, // 원인 찾기는 왔다 갔다 하므로 여유를 준다
|
|
73
|
-
say:
|
|
74
|
-
|
|
75
|
-
|
|
133
|
+
say: [
|
|
134
|
+
"지금 하는 일은 **원인 찾기**다. 짐작으로 고치지 마라.",
|
|
135
|
+
"",
|
|
136
|
+
"이 순서를 지켜라.",
|
|
137
|
+
" 1. 증상을 한 문장으로 다시 적는다 — 무엇을 했을 때 무엇이 일어나는가.",
|
|
138
|
+
" 2. 재현 방법을 확보한다. 없으면 만든다. 재현 못 하면 고쳤는지도 알 수 없다.",
|
|
139
|
+
" 3. 가설을 2~3개 세운다. 각각 \"이게 맞다면 무엇이 보여야 하는가\" 를 같이 적는다.",
|
|
140
|
+
" 4. 하나씩 실제로 확인한다 — 로그를 보고, 작게 돌려 보고, 값을 찍어 본다.",
|
|
141
|
+
" 확인한 것만 사실이다. 확인 안 한 것은 아직 가설이다.",
|
|
142
|
+
" 5. 원인을 짚을 때는 증거를 같이 댄다. \"아마\" 로 시작하는 원인은 원인이 아니다.",
|
|
143
|
+
" 6. 고친 뒤 2번의 재현 절차로 다시 확인한다. 안 고쳐졌으면 3번으로 돌아간다.",
|
|
144
|
+
"",
|
|
145
|
+
"한 번에 여러 곳을 고치지 마라. 무엇이 고쳤는지 알 수 없게 된다.",
|
|
146
|
+
].join('\n'),
|
|
76
147
|
},
|
|
77
148
|
|
|
78
149
|
plan: {
|
|
@@ -85,9 +156,26 @@ export const MODES = {
|
|
|
85
156
|
effort: 'deep',
|
|
86
157
|
think: 'high',
|
|
87
158
|
steps: 16,
|
|
88
|
-
say:
|
|
89
|
-
|
|
90
|
-
|
|
159
|
+
say: [
|
|
160
|
+
"지금 하는 일은 **계획 세우기**다. 파일을 바꾸는 도구는 주어지지 않았다.",
|
|
161
|
+
"코드를 고치려 들지 마라. 계획을 내고 멈춘다.",
|
|
162
|
+
"",
|
|
163
|
+
"먼저 확인하라 — 지금 무엇이 어떻게 되어 있는지 모르면 계획이 아니라 희망이다.",
|
|
164
|
+
" Glob/Grep 으로 관련 파일을 찾고, Read 로 실제로 읽어라.",
|
|
165
|
+
"",
|
|
166
|
+
"그 다음 이 차례로 적어라.",
|
|
167
|
+
" 1. 목표 — 무엇이 끝나면 다 된 것인가 (확인할 수 있는 문장으로)",
|
|
168
|
+
" 2. 지금 상태 — 관련된 파일과 지금 동작 (경로:줄 로 짚어라)",
|
|
169
|
+
" 3. 바꿀 것 — 파일별로 무엇을 왜",
|
|
170
|
+
" 4. 순서 — 단계별로. 각 단계는 따로 확인할 수 있는 크기로 쪼개라",
|
|
171
|
+
" 5. 위험 — 무엇이 깨질 수 있고, 깨지면 어떻게 되돌리는가",
|
|
172
|
+
" 6. 확인 방법 — 무엇을 돌려 보면 됐는지 알 수 있는가",
|
|
173
|
+
"",
|
|
174
|
+
"단계는 TodoWrite 로도 적어라. 승인 뒤 그대로 이어서 하게 된다.",
|
|
175
|
+
"모르는 것이 있으면 지어내지 말고 \"이건 확인이 필요하다\" 고 적어라.",
|
|
176
|
+
"",
|
|
177
|
+
"마지막에 \"이대로 진행할까요?\" 로 끝내라. 승인을 받으면 /code 로 바꿔 실행한다.",
|
|
178
|
+
].join('\n'),
|
|
91
179
|
},
|
|
92
180
|
|
|
93
181
|
orchestrator: {
|
|
@@ -100,14 +188,24 @@ export const MODES = {
|
|
|
100
188
|
effort: 'save',
|
|
101
189
|
think: null,
|
|
102
190
|
steps: 40, // 여러 갈래를 끝까지 끌고 가야 한다
|
|
103
|
-
say:
|
|
104
|
-
|
|
105
|
-
|
|
191
|
+
say: [
|
|
192
|
+
"지금 하는 일은 **큰 일을 끝까지 끌고 가기**다.",
|
|
193
|
+
"",
|
|
194
|
+
" 1. 시작하자마자 TodoWrite 로 전체를 단계로 쪼개 적어라. 머릿속에만 두지 마라.",
|
|
195
|
+
" 단계는 각각 따로 확인할 수 있는 크기여야 한다.",
|
|
196
|
+
" 2. 한 번에 하나만 진행 중으로 둬라. 끝나면 바로 표시하고 다음으로 넘어가라.",
|
|
197
|
+
" 3. 각 단계 끝에서 확인하라. 확인 없이 넘어가면 어디서 어긋났는지 못 찾는다.",
|
|
198
|
+
" 4. 막히면 멈추고 무엇에 막혔는지 보고하라. 우회로를 몰래 타지 마라.",
|
|
199
|
+
" 5. 계획이 틀린 것을 알게 되면 목록을 고쳐라. 틀린 계획을 끝까지 밀지 마라.",
|
|
200
|
+
"",
|
|
201
|
+
"다 끝나면 무엇을 했는지, 무엇을 못 했는지 같이 요약하라.",
|
|
202
|
+
"못 한 것을 빼고 요약하지 마라.",
|
|
203
|
+
].join('\n'),
|
|
106
204
|
},
|
|
107
205
|
};
|
|
108
206
|
|
|
109
|
-
export const ORDER = ['code', 'plan', 'architect', 'debug', 'ask', 'orchestrator'];
|
|
110
|
-
export const DEFAULT = '
|
|
207
|
+
export const ORDER = ['auto', 'code', 'plan', 'architect', 'debug', 'ask', 'orchestrator'];
|
|
208
|
+
export const DEFAULT = 'auto';
|
|
111
209
|
|
|
112
210
|
/** 이름을 관대하게 받는다. 한글·영문·줄임말 다 통한다. */
|
|
113
211
|
export function normalize(v) {
|
|
@@ -115,6 +213,7 @@ export function normalize(v) {
|
|
|
115
213
|
if (!s) return null;
|
|
116
214
|
if (MODES[s]) return s;
|
|
117
215
|
const 별명 = {
|
|
216
|
+
'종합': 'auto', '자동': 'auto', '기본': 'auto', 'auto': 'auto', '맡김': 'auto',
|
|
118
217
|
'코드': 'code', 'c': 'code',
|
|
119
218
|
'계획': 'plan', '플랜': 'plan', 'p': 'plan',
|
|
120
219
|
'설계': 'architect', '아키': 'architect', 'arch': 'architect', 'a': 'architect',
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// 사용자가 무엇을 시켰는지 보고 알맞은 작업 모드를 고른다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 규칙으로 하나:
|
|
4
|
+
// 모델에게 "이건 무슨 일이냐" 고 한 번 더 물을 수도 있다. 그런데 그러면
|
|
5
|
+
// 한마디 할 때마다 왕복이 하나 늘고, 작은 로컬 모델은 이런 분류를 잘 못한다.
|
|
6
|
+
// 느려지고 틀리기까지 하면 자동으로 하는 의미가 없다.
|
|
7
|
+
//
|
|
8
|
+
// 그래서 낱말로 고른다. 대신 두 가지를 지킨다.
|
|
9
|
+
// 1) 애매하면 안 바꾼다. 종합 모드에 그대로 둔다.
|
|
10
|
+
// 잘못 바꾸는 것보다 안 바꾸는 편이 낫다 — 특히 읽기 전용 모드로
|
|
11
|
+
// 잘못 보내면 사용자는 "왜 파일을 안 고쳐?" 하고 막힌다.
|
|
12
|
+
// 2) 왜 그렇게 봤는지 남긴다. 화면에 그 이유가 같이 뜬다.
|
|
13
|
+
//
|
|
14
|
+
// 사용자가 직접 모드를 고르면 이건 아예 안 돈다. 사람이 고른 것을 뒤집지 않는다.
|
|
15
|
+
|
|
16
|
+
// 낱말과 점수. 큰 점수일수록 그 모드라는 증거가 강하다는 뜻이다.
|
|
17
|
+
//
|
|
18
|
+
// 한국어를 먼저 두되 영어도 같이 본다 — 오류 메시지를 그대로 붙여넣는 일이 흔하다.
|
|
19
|
+
const 표 = {
|
|
20
|
+
debug: [
|
|
21
|
+
// 고장은 신호가 뚜렷하다. 그리고 디버그는 파일을 고칠 수 있으므로
|
|
22
|
+
// 잘못 골라도 막히지 않는다. 그래서 세게 준다.
|
|
23
|
+
[/왜\s*안\s*(되|돼|됨)/, 5], [/안\s*(되는|돼는|됩니다|되네|돼네|돼요|되요)/, 4],
|
|
24
|
+
[/작동\s*안/, 4], [/동작\s*안/, 4],
|
|
25
|
+
[/에러|오류|예외|익셉션/, 4], [/버그/, 4],
|
|
26
|
+
[/실패/, 3], [/터(져|졌|진다)/, 3], [/죽(어|었|는다|네)/, 3],
|
|
27
|
+
[/멈(춰|췄|춘다|추)/, 3], [/먹통/, 3], [/이상(해|하다|한데)/, 3],
|
|
28
|
+
[/재현/, 3], [/스택\s*트레이스|stack\s*trace|traceback/i, 4],
|
|
29
|
+
// 'TypeError' 처럼 앞에 글자가 붙어 오는 일이 훨씬 흔하다.
|
|
30
|
+
// \berror\b 로 잡으면 정작 붙여넣은 진짜 오류 메시지를 놓친다.
|
|
31
|
+
[/error/i, 3], [/\bexception\b|\bcrash|\bfail(s|ed|ing)?\b|\bbroken\b/i, 3],
|
|
32
|
+
[/고장/, 3], [/왜\s*이(래|러|렇게\s*나)/, 2],
|
|
33
|
+
[/왜\s*(못|안)/, 3], [/뭐\s*때문(이|에)/, 3],
|
|
34
|
+
],
|
|
35
|
+
|
|
36
|
+
plan: [
|
|
37
|
+
// '먼저' 와 붙어야 계획이다. 그냥 '어떻게' 는 설명일 수도 있다.
|
|
38
|
+
[/계획/, 5], [/플랜|\bplan\b/i, 5],
|
|
39
|
+
[/먼저\s*(정리|생각|살펴|파악|검토|짜|세)/, 4],
|
|
40
|
+
[/어떤\s*순서/, 4], [/순서(를|대로)?\s*(정|짜|잡)/, 3],
|
|
41
|
+
[/단계(를|로)?\s*(나눠|쪼개|정리)/, 3],
|
|
42
|
+
[/로드맵|\broadmap\b/i, 5],
|
|
43
|
+
[/방향(을|만)?\s*(잡|정)/, 3],
|
|
44
|
+
[/하기\s*전에\s*(정리|살펴|검토)/, 3],
|
|
45
|
+
[/무엇부터|뭐부터/, 3],
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
architect: [
|
|
49
|
+
[/설계/, 5], [/아키텍(처|쳐)|architecture/i, 5],
|
|
50
|
+
[/구조(를|가|는)?\s*(어떻게|바꾸|잡|짜|개선)/, 5],
|
|
51
|
+
[/어떻게\s*(나누|쪼개|짜)/, 5],
|
|
52
|
+
[/모듈(로|을)?\s*(나누|분리)/, 3],
|
|
53
|
+
[/의존(성|\s*방향)/, 3], [/레이어|계층/, 3],
|
|
54
|
+
[/어느\s*(쪽|것)이\s*(나을|좋을)/, 3], [/선택지|대안/, 3],
|
|
55
|
+
[/패턴(을|이)?\s*(쓰|적용|고르)/, 2],
|
|
56
|
+
[/\bdesign\b|\bstructure\b|\brefactor(ing)?\s*(방향|plan)/i, 3],
|
|
57
|
+
],
|
|
58
|
+
|
|
59
|
+
ask: [
|
|
60
|
+
// 설명은 아무것도 안 바꾸는 모드라 잘못 고르면 답답해진다. 신호를 엄격히 본다.
|
|
61
|
+
[/뭐(야|예요|입니까|지)\s*\??$/m, 5], [/무엇(인가|입니까|이야)/, 5],
|
|
62
|
+
[/설명(해|좀|을)/, 5], [/알려\s*(줘|주세요|줄래)/, 3],
|
|
63
|
+
[/어떻게\s*(동작|작동|돌아가)/, 5],
|
|
64
|
+
[/무슨\s*(뜻|의미)|의미(가|는)\s*뭐/, 5],
|
|
65
|
+
[/차이(가|는|점)/, 3],
|
|
66
|
+
[/궁금/, 3], [/왜\s*이렇게\s*(만들|짰|되어)/, 3],
|
|
67
|
+
[/\bwhat\s+is\b|\bexplain\b|\bhow\s+does\b/i, 4],
|
|
68
|
+
[/읽어(만)?\s*보고\s*(설명|알려)/, 4],
|
|
69
|
+
],
|
|
70
|
+
|
|
71
|
+
orchestrator: [
|
|
72
|
+
// 총괄의 신호는 낱말 하나가 아니라 '넓이' 다. 그래서 하나하나는 약하게 두고
|
|
73
|
+
// 여럿이 겹칠 때 문턱을 넘게 한다 — 실제로 큰 일에는 이런 말이 같이 나온다.
|
|
74
|
+
// "전체 로그 형식을 다 통일해줘" 는 '전체' 와 '통일' 이 겹쳐야 총괄이 된다.
|
|
75
|
+
[/전부|모두|싹\s*다|통째로/, 3],
|
|
76
|
+
[/전체/, 3],
|
|
77
|
+
[/하나씩/, 3],
|
|
78
|
+
[/끝까지/, 3], [/마무리까지/, 3],
|
|
79
|
+
[/여러\s*(개|군데|파일|단계|곳)/, 3],
|
|
80
|
+
[/순서대로/, 3],
|
|
81
|
+
[/통일(해|시켜|좀)|일괄/, 2],
|
|
82
|
+
[/흩어진|여기저기/, 2],
|
|
83
|
+
[/\bend[- ]?to[- ]?end\b/i, 3],
|
|
84
|
+
],
|
|
85
|
+
|
|
86
|
+
code: [
|
|
87
|
+
[/고쳐|고쳐줘|수정(해|좀)/, 4],
|
|
88
|
+
[/만들어|추가(해|좀)|넣어(줘|주세요)/, 4],
|
|
89
|
+
[/구현(해|좀)/, 5],
|
|
90
|
+
[/바꿔(줘|주세요)?|변경(해|좀)/, 3],
|
|
91
|
+
[/지워(줘)?|삭제(해|좀)/, 3],
|
|
92
|
+
[/이름\s*바꿔|리네임|rename/i, 3],
|
|
93
|
+
[/옮겨(줘)?/, 2],
|
|
94
|
+
[/리팩터(링)?(해|좀)/, 3],
|
|
95
|
+
[/\bimplement\b|\badd\b|\bfix\b|\bwrite\b/i, 2],
|
|
96
|
+
[/써(줘|주세요)/, 3],
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 이만큼은 되어야 바꾼다. 못 미치면 종합 모드 그대로 둔다.
|
|
101
|
+
//
|
|
102
|
+
// 읽기 전용 모드는 문턱을 높인다. 잘못 보내면 파일을 못 고쳐서 사용자가 막힌다.
|
|
103
|
+
// 파일을 고칠 수 있는 모드는 잘못 골라도 일이 막히지는 않으므로 낮게 둔다.
|
|
104
|
+
const 문턱 = {
|
|
105
|
+
debug: 3,
|
|
106
|
+
code: 3,
|
|
107
|
+
orchestrator: 4,
|
|
108
|
+
plan: 5,
|
|
109
|
+
architect: 5,
|
|
110
|
+
ask: 5,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 이 한마디가 무슨 일인지 고른다.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text 사용자가 친 말
|
|
117
|
+
* @returns {{ mode: string|null, score: number, why: string, 점수들: object }}
|
|
118
|
+
* mode 가 null 이면 '모르겠다' 는 뜻이다 — 종합 모드에 그대로 둔다.
|
|
119
|
+
*/
|
|
120
|
+
export function route(text) {
|
|
121
|
+
const s = String(text ?? '');
|
|
122
|
+
const 점수들 = {};
|
|
123
|
+
const 근거 = {};
|
|
124
|
+
|
|
125
|
+
for (const [mode, 규칙들] of Object.entries(표)) {
|
|
126
|
+
let 합 = 0;
|
|
127
|
+
const 걸린것 = [];
|
|
128
|
+
for (const [re, 점] of 규칙들) {
|
|
129
|
+
const m = s.match(re);
|
|
130
|
+
if (!m) continue;
|
|
131
|
+
합 += 점;
|
|
132
|
+
걸린것.push(m[0].trim());
|
|
133
|
+
}
|
|
134
|
+
점수들[mode] = 합;
|
|
135
|
+
근거[mode] = 걸린것;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const 순위 = Object.entries(점수들).sort((a, b) => b[1] - a[1]);
|
|
139
|
+
const [으뜸, 점수] = 순위[0] ?? [null, 0];
|
|
140
|
+
if (!으뜸 || 점수 < (문턱[으뜸] ?? 4)) {
|
|
141
|
+
return { mode: null, score: 점수, why: '무슨 일인지 뚜렷하지 않음', 점수들 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 1·2등이 붙으면 고르지 않는다. 반반인 것을 억지로 정하면 절반은 틀린다.
|
|
145
|
+
const 둘째 = 순위[1]?.[1] ?? 0;
|
|
146
|
+
if (점수 - 둘째 < 2 && 둘째 >= (문턱[순위[1][0]] ?? 4)) {
|
|
147
|
+
return { mode: null, score: 점수, why: `${으뜸} 과 ${순위[1][0]} 가 비슷함`, 점수들 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
mode: 으뜸,
|
|
152
|
+
score: 점수,
|
|
153
|
+
why: 근거[으뜸].slice(0, 2).map((x) => `'${x}'`).join(', '),
|
|
154
|
+
점수들,
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/agent/session.js
CHANGED
|
@@ -32,6 +32,9 @@ export class Session {
|
|
|
32
32
|
this.root = root;
|
|
33
33
|
this.mode = mode; // 승인 정책 — 얼마나 물어보나 (auto/confirm/strict)
|
|
34
34
|
this.work = work ?? WORK_DEFAULT; // 작업 모드 — 무슨 일을 하는 중인가 (modes.js)
|
|
35
|
+
// 이번 한마디에만 쓸 모드. 종합 모드일 때 요청을 보고 골라 넣는다 (agent/route.js).
|
|
36
|
+
// 기본 모드(this.work)는 안 건드린다 — 다음 한마디는 다시 처음부터 고른다.
|
|
37
|
+
this.routed = null;
|
|
35
38
|
// 사용자 수준 — 화면에 무엇을 내놓을지만 정한다. 안전 장치는 안 바꾼다 (ui/level.js)
|
|
36
39
|
this.level = normLevel(level) ?? LEVEL_DEFAULT;
|
|
37
40
|
this.think = think; // 기준 강도
|
|
@@ -60,12 +63,23 @@ export class Session {
|
|
|
60
63
|
return null;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
/**
|
|
67
|
+
* 지금 이 순간 실제로 쓰는 작업 모드.
|
|
68
|
+
*
|
|
69
|
+
* 종합 모드에서는 한마디마다 골라 넣은 것(routed)이 있고, 그때는 그것이 답이다.
|
|
70
|
+
* 직접 고른 모드가 있으면 routed 는 비어 있으므로 기본 모드가 그대로 답이 된다.
|
|
71
|
+
* 도구·추론·프롬프트가 전부 이 값을 봐야 한다. 하나라도 빠뜨리면 어긋난다.
|
|
72
|
+
*/
|
|
73
|
+
effectiveWork() {
|
|
74
|
+
return this.routed ?? this.work;
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
systemPrompt() {
|
|
64
78
|
const parts = [BASE_RULES];
|
|
65
79
|
parts.push(`\n작업 폴더: ${this.root}\n이 폴더 밖의 파일은 읽지도 쓰지도 못한다.`);
|
|
66
80
|
|
|
67
81
|
// 지금 무슨 일을 하는 중인지. 도구 목록도 이 모드에 맞춰 이미 걸러져 있다.
|
|
68
|
-
const w = workMode(this.
|
|
82
|
+
const w = workMode(this.effectiveWork());
|
|
69
83
|
parts.push(`\n--- 지금 모드: ${w.name} (${w.en}) ---\n${w.say}`);
|
|
70
84
|
if (this.rules) parts.push(`\n--- ${this.rules.name} (사용자 규칙, 위 원칙보다 우선) ---\n${this.rules.text}`);
|
|
71
85
|
const listed = this.listedSkills();
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// 모델이 한 번에 받아 줄 수 있는 길이(컨텍스트)를 서버에서 알아낸다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 파일을 따로 뒀는가:
|
|
4
|
+
//
|
|
5
|
+
// 이 숫자 하나가 프로그램 전체를 좌우한다. 이게 작으면 파일을 몇 개 못 읽히고,
|
|
6
|
+
// 대화가 금방 접히고, 한 번에 쓸 수 있는 답 길이도 같이 줄어든다(effort.js 가
|
|
7
|
+
// 이 값에서 상한을 계산한다). 예전에는 못 알아내면 32768 로 뒀는데, 요즘
|
|
8
|
+
// 로컬 모델은 262,144 · 655,360 처럼 훨씬 크다. 32k 로 깔고 앉으면 모델이
|
|
9
|
+
// 가진 것의 5% 만 쓰는 셈이다.
|
|
10
|
+
//
|
|
11
|
+
// 문제는 서버마다 이 숫자를 다른 이름, 다른 자리에 둔다는 것이다. 그래서
|
|
12
|
+
// 한 군데만 보지 않고 아는 자리를 전부 훑는다. 한 번에 다 못 찾아도
|
|
13
|
+
// 부분만 찾으면 그걸 쓴다.
|
|
14
|
+
//
|
|
15
|
+
// OpenAI 호환 /v1/models/{id} 또는 /v1/models 목록 안의 그 항목
|
|
16
|
+
// LM Studio /api/v0/models (max_context_length · loaded_context_length)
|
|
17
|
+
// llama.cpp /props (n_ctx)
|
|
18
|
+
// vLLM /v1/models (max_model_len)
|
|
19
|
+
// Ollama /api/show (….context_length)
|
|
20
|
+
//
|
|
21
|
+
// '올린 길이' 와 '모델 최대' 를 구분한다. LM Studio 는 모델이 655,360 까지
|
|
22
|
+
// 되더라도 8,192 로 올려 둘 수 있다. 그 상태에서 655,360 을 믿고 보내면
|
|
23
|
+
// 서버가 거절한다. 그래서 실제로 쓸 값은 '올린 길이' 로 잡고, '모델 최대' 는
|
|
24
|
+
// 따로 알려 준다 — 사용자가 서버에서 더 올린 다음 /ctx 로 맞출 수 있게.
|
|
25
|
+
import { req, headersFor } from './http.js';
|
|
26
|
+
|
|
27
|
+
// 이 이름들 중 하나면 컨텍스트 길이다. 순서가 곧 우선순위다.
|
|
28
|
+
const 최대이름 = [
|
|
29
|
+
'max_context_length', // LM Studio
|
|
30
|
+
'context_window', // 일부 게이트웨이
|
|
31
|
+
'context_length', // llama.cpp·HF 계열
|
|
32
|
+
'max_model_len', // vLLM
|
|
33
|
+
'max_input_tokens', // LiteLLM·OpenRouter 계열
|
|
34
|
+
'n_ctx', // llama.cpp
|
|
35
|
+
'max_position_embeddings', // 모델 config 원본
|
|
36
|
+
'max_sequence_length',
|
|
37
|
+
'max_seq_len',
|
|
38
|
+
];
|
|
39
|
+
const 올린이름 = ['loaded_context_length', 'n_ctx', 'context_length'];
|
|
40
|
+
|
|
41
|
+
// 사람이 알아볼 수 없는 값은 안 받는다. 512 미만은 오독, 1000만 초과는 단위 착각.
|
|
42
|
+
const 최소 = 512;
|
|
43
|
+
const 최대허용 = 10_000_000;
|
|
44
|
+
|
|
45
|
+
function 성한수(v) {
|
|
46
|
+
const n = Number(v);
|
|
47
|
+
if (!Number.isFinite(n)) return null;
|
|
48
|
+
const i = Math.floor(n);
|
|
49
|
+
return i >= 최소 && i <= 최대허용 ? i : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 객체 어디에 박혀 있어도 찾는다.
|
|
54
|
+
*
|
|
55
|
+
* 서버가 { data: { meta: { config: { max_position_embeddings: 655360 } } } } 처럼
|
|
56
|
+
* 깊이 넣어 두는 일이 흔하다. 그래서 이름으로 훑는다. 다만 아무 데나 파고들면
|
|
57
|
+
* 엉뚱한 숫자를 집으므로 깊이를 5 로 막는다.
|
|
58
|
+
*/
|
|
59
|
+
function 파보기(obj, 이름들, depth = 0) {
|
|
60
|
+
if (!obj || typeof obj !== 'object' || depth > 5) return null;
|
|
61
|
+
for (const key of 이름들) {
|
|
62
|
+
const v = 성한수(obj[key]);
|
|
63
|
+
if (v) return v;
|
|
64
|
+
}
|
|
65
|
+
// 이름이 정확히 안 맞으면 '…context_length' 처럼 끝나는 것도 본다 (Ollama).
|
|
66
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
67
|
+
if (/(^|[._])(context_length|context_window|n_ctx)$/i.test(k)) {
|
|
68
|
+
const n = 성한수(v);
|
|
69
|
+
if (n) return n;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const v of Object.values(obj)) {
|
|
73
|
+
if (v && typeof v === 'object') {
|
|
74
|
+
const found = 파보기(v, 이름들, depth + 1);
|
|
75
|
+
if (found) return found;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 목록 응답에서 이 모델 항목만 골라낸다.
|
|
82
|
+
function 목록에서찾기(json, model) {
|
|
83
|
+
const list = json?.data ?? json?.models ?? (Array.isArray(json) ? json : []);
|
|
84
|
+
if (!Array.isArray(list)) return null;
|
|
85
|
+
return list.find((m) => (m?.id ?? m?.name ?? m?.model) === model) ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 서버에 물어 컨텍스트 길이를 알아낸다.
|
|
90
|
+
*
|
|
91
|
+
* @param {{kind:string, base:string, auth:string, key:string, model:string}} conn
|
|
92
|
+
* @returns {Promise<{value:number|null, max:number|null, loaded:number|null, source:string|null, tried:Array}>}
|
|
93
|
+
*/
|
|
94
|
+
export async function probeCtx(conn, { timeout = 12000 } = {}) {
|
|
95
|
+
const H = () => headersFor(conn.auth, conn.key);
|
|
96
|
+
const base = String(conn.base ?? '').replace(/\/+$/, '');
|
|
97
|
+
const origin = base.replace(/\/v\d+$/, '').replace(/\/api$/, '');
|
|
98
|
+
const model = conn.model;
|
|
99
|
+
const tried = [];
|
|
100
|
+
let max = null;
|
|
101
|
+
let loaded = null;
|
|
102
|
+
let source = null;
|
|
103
|
+
|
|
104
|
+
const 본다 = async (label, url, opts = {}) => {
|
|
105
|
+
if (max && loaded) return null;
|
|
106
|
+
try {
|
|
107
|
+
const r = await req(url, { headers: H(), timeout, ...opts });
|
|
108
|
+
tried.push({ label, url, status: r.status ?? 0, ok: !!r.ok });
|
|
109
|
+
return r.ok ? r.json : null;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
tried.push({ label, url, status: 0, ok: false, why: String(err?.message ?? err) });
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const 담기 = (json, label, { 목록 = false } = {}) => {
|
|
117
|
+
if (!json) return;
|
|
118
|
+
const 대상 = 목록 ? 목록에서찾기(json, model) : json;
|
|
119
|
+
if (!대상) return;
|
|
120
|
+
const m = 파보기(대상, 최대이름);
|
|
121
|
+
const l = 파보기(대상, 올린이름);
|
|
122
|
+
if (m && !max) { max = m; source = label; }
|
|
123
|
+
if (l && !loaded) loaded = l;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (conn.kind === 'ollama') {
|
|
127
|
+
담기(await 본다('Ollama /api/show', `${origin}/api/show`, { method: 'POST', body: { model } }), 'Ollama 모델 정보');
|
|
128
|
+
} else {
|
|
129
|
+
// 1) 이 모델만 콕 집어 묻는다. 있으면 가장 정확하다.
|
|
130
|
+
담기(await 본다('모델 상세', `${base}/models/${encodeURIComponent(model)}`), '모델 상세');
|
|
131
|
+
|
|
132
|
+
// 2) LM Studio 는 자기 규격에만 '올린 길이' 를 준다. 이게 실제로 쓸 값이다.
|
|
133
|
+
담기(await 본다('LM Studio 상세', `${origin}/api/v0/models/${encodeURIComponent(model)}`), 'LM Studio');
|
|
134
|
+
담기(await 본다('LM Studio 목록', `${origin}/api/v0/models`), 'LM Studio', { 목록: true });
|
|
135
|
+
|
|
136
|
+
// 3) 목록 응답 안에 들어 있는 경우 (vLLM 의 max_model_len 이 여기 있다)
|
|
137
|
+
담기(await 본다('모델 목록', `${base}/models`), '모델 목록', { 목록: true });
|
|
138
|
+
|
|
139
|
+
// 4) llama.cpp 서버는 /props 로만 알려 준다.
|
|
140
|
+
const props = await 본다('llama.cpp /props', `${origin}/props`);
|
|
141
|
+
if (props) {
|
|
142
|
+
const n = 파보기(props, ['n_ctx', 'context_length']);
|
|
143
|
+
if (n) { if (!max) { max = n; source = 'llama.cpp'; } if (!loaded) loaded = n; }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 실제로 쓸 값: 올려 둔 길이가 있으면 그것. 없으면 모델 최대.
|
|
148
|
+
const value = loaded ?? max ?? null;
|
|
149
|
+
return { value, max, loaded, source, tried };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* "655360" · "655k" · "128K" · "1m" 을 숫자로. 못 읽으면 null.
|
|
154
|
+
*
|
|
155
|
+
* k/m 을 받는 이유는 단순하다 — 655360 을 손으로 치면 자릿수를 틀린다.
|
|
156
|
+
*/
|
|
157
|
+
export function parseSize(text) {
|
|
158
|
+
const s = String(text ?? '').trim().replace(/[,_\s]/g, '').toLowerCase();
|
|
159
|
+
const m = /^(\d+(?:\.\d+)?)([km])?$/.exec(s);
|
|
160
|
+
if (!m) return null;
|
|
161
|
+
const mult = m[2] === 'm' ? 1024 * 1024 : m[2] === 'k' ? 1024 : 1;
|
|
162
|
+
return 성한수(Number(m[1]) * mult);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 33k · 655k · 1.0M — 화면에 넣을 짧은 표기 */
|
|
166
|
+
export function fmtSize(n) {
|
|
167
|
+
const v = Number(n) || 0;
|
|
168
|
+
if (v < 1000) return String(v);
|
|
169
|
+
if (v < 1024 * 1024) return Math.round(v / 1024) + 'k';
|
|
170
|
+
return (v / (1024 * 1024)).toFixed(1) + 'M';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 서버가 끝내 안 알려 줄 때 쓰는 값. 옛날 32768 보다는 요즘 기본에 가깝다. */
|
|
174
|
+
export const 기본값 = 32768;
|
package/src/backend/http.js
CHANGED
|
@@ -11,6 +11,29 @@ export const AUTH_STYLES = [
|
|
|
11
11
|
{ id: 'none', label: '인증 없음', apply: () => {} },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 열어 둔 연결을 닫는다. 프로그램을 끝내기 직전에 부른다.
|
|
16
|
+
*
|
|
17
|
+
* fetch 는 연결을 재사용하려고 소켓을 살려 둔다(keep-alive). 그래서 할 일이
|
|
18
|
+
* 끝나도 프로세스가 저절로 안 끝난다. 예전에는 그걸 process.exit() 으로
|
|
19
|
+
* 잘라 냈는데, 윈도우에서 닫는 중인 핸들을 두고 끊으면 libuv 가 abort 한다.
|
|
20
|
+
*
|
|
21
|
+
* Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), src\win\async.c
|
|
22
|
+
*
|
|
23
|
+
* 실제로 `deel scan` 이 결과를 다 찍고 나서 이렇게 죽었다. 화면에는 정상으로
|
|
24
|
+
* 보이는데 종료코드는 3221226505(0xC0000409) 였다.
|
|
25
|
+
*
|
|
26
|
+
* 그래서 잘라 내는 대신 닫는다. 이 자리는 Node 내부 이름이라 없을 수도 있으므로,
|
|
27
|
+
* 없으면 조용히 넘어간다 — 그때는 부르는 쪽의 시간제한이 받아 준다.
|
|
28
|
+
*/
|
|
29
|
+
export function closeConnections() {
|
|
30
|
+
try {
|
|
31
|
+
const d = globalThis[Symbol.for('undici.globalDispatcher.1')];
|
|
32
|
+
if (d && typeof d.close === 'function') return d.close().catch(() => {});
|
|
33
|
+
} catch { /* 없으면 그만 */ }
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
export function headersFor(authStyle, key, extra = {}) {
|
|
15
38
|
const h = { 'Content-Type': 'application/json', Accept: 'application/json', ...extra };
|
|
16
39
|
const style = AUTH_STYLES.find((s) => s.id === authStyle) ?? AUTH_STYLES[0];
|
package/src/backend/probe.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// warn 되긴 하는데 조건이 붙음
|
|
6
6
|
// skip 앞 검사가 실패해 확인 불가
|
|
7
7
|
import { req, headersFor, serverMessage } from './http.js';
|
|
8
|
+
import { probeCtx } from './ctxsize.js';
|
|
8
9
|
|
|
9
10
|
const READ_TOOL = {
|
|
10
11
|
type: 'function',
|
|
@@ -146,7 +147,7 @@ export async function probe(conn, onStep = () => {}) {
|
|
|
146
147
|
label: '시스템 메시지',
|
|
147
148
|
status: sys.ok ? (sysHit ? 'ok' : 'warn') : 'no',
|
|
148
149
|
detail: sys.ok
|
|
149
|
-
? sysHit ? '지시를 따름' : '전달은 되나 모델이 잘 안 따름 — 규칙·스킬이 약하게
|
|
150
|
+
? sysHit ? '지시를 따름' : '전달은 되나 모델이 잘 안 따름 — 규칙·스킬이 약하게 적용됩니다'
|
|
150
151
|
: serverMessage(sys),
|
|
151
152
|
ms: sys.ms,
|
|
152
153
|
});
|
|
@@ -330,26 +331,26 @@ export async function probe(conn, onStep = () => {}) {
|
|
|
330
331
|
facts.think = differs;
|
|
331
332
|
|
|
332
333
|
// 8. 컨텍스트 길이 — 파일을 몇 개까지 한 번에 읽힐 수 있느냐.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const k = Object.keys(info).find((x) => /context_length/.test(x));
|
|
339
|
-
if (k) { ctx = info[k]; ctxNote = '모델 정보에서 읽음'; }
|
|
340
|
-
} else {
|
|
341
|
-
const m = await req(`${base}/models/${encodeURIComponent(model)}`, { headers: H(), timeout: 15000 });
|
|
342
|
-
ctx = m.json?.context_window ?? m.json?.max_context_length ?? m.json?.context_length ?? null;
|
|
343
|
-
if (ctx) ctxNote = '모델 정보에서 읽음';
|
|
344
|
-
}
|
|
334
|
+
//
|
|
335
|
+
// 한 자리만 보지 않는다. 서버마다 이름도 자리도 다르다 (ctxsize.js 참고).
|
|
336
|
+
// 여기서 작게 잡으면 프로그램 전체가 작아진다 — 답 길이 상한까지 이 값에서 나온다.
|
|
337
|
+
const 길이 = await probeCtx({ kind: shape, base, auth, key, model });
|
|
338
|
+
const ctx = 길이.value;
|
|
345
339
|
add({
|
|
346
340
|
id: 'ctx',
|
|
347
341
|
label: '컨텍스트 길이',
|
|
348
342
|
status: ctx ? 'ok' : 'warn',
|
|
349
|
-
detail: ctx
|
|
343
|
+
detail: ctx
|
|
344
|
+
? `${ctx.toLocaleString()} 토큰 (${길이.source ?? '모델 정보'}에서 읽음)`
|
|
345
|
+
+ (길이.max && 길이.loaded && 길이.max > 길이.loaded
|
|
346
|
+
? ` · 이 모델은 ${길이.max.toLocaleString()} 까지 되는데 지금 ${길이.loaded.toLocaleString()} 로 올려 두셨습니다`
|
|
347
|
+
: '')
|
|
348
|
+
: '서버가 알려주지 않음 — /ctx 로 직접 지정하세요',
|
|
350
349
|
ms: 0,
|
|
351
350
|
});
|
|
352
351
|
facts.ctx = ctx;
|
|
352
|
+
facts.ctxMax = 길이.max ?? null;
|
|
353
|
+
facts.ctxLoaded = 길이.loaded ?? null;
|
|
353
354
|
|
|
354
355
|
return { facts, results };
|
|
355
356
|
}
|