deel-local-cli 1.5.8 → 1.6.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 +23 -13
- package/README.md +22 -12
- package/bin/deel.js +10 -0
- package/package.json +2 -2
- package/src/acp/map.js +143 -0
- package/src/acp/serve.js +160 -13
- package/src/agent/commit.js +511 -0
- package/src/agent/compact.js +269 -226
- package/src/agent/loop.js +154 -12
- package/src/agent/mention.js +56 -10
- package/src/agent/review.js +192 -0
- package/src/agent/session.js +20 -2
- package/src/agent/threads.js +1 -1
- package/src/backend/adapter.js +342 -280
- package/src/backend/azure.js +151 -0
- package/src/backend/ctxsize.js +19 -0
- package/src/backend/detect.js +106 -0
- package/src/backend/http.js +350 -30
- package/src/backend/probe.js +48 -1
- package/src/backend/proxy.js +151 -0
- package/src/backend/quota.js +133 -0
- package/src/backend/retry.js +132 -0
- package/src/backend/vision.js +185 -0
- package/src/commands.js +341 -8
- package/src/completion.js +264 -0
- package/src/config.js +117 -1
- package/src/i18n/en.js +13 -1
- package/src/i18n/index.js +22 -3
- package/src/i18n/ja.js +266 -0
- package/src/i18n/ko.js +12 -0
- package/src/i18n/zh.js +266 -0
- package/src/oneshot.js +28 -3
- package/src/pack/sbom.js +25 -0
- package/src/pack/selfpack.js +9 -3
- package/src/plugins/manage.js +34 -7
- package/src/repl.js +78 -6
- package/src/report.js +26 -2
- package/src/safety/guard.js +13 -2
- package/src/safety/keystore.js +237 -0
- package/src/safety/network.js +76 -14
- package/src/safety/policy.js +209 -0
- package/src/safety/secrets.js +32 -0
- package/src/setup.js +40 -7
- package/src/tools/clipboard.js +178 -0
- package/src/tools/fastgrep.js +217 -0
- package/src/tools/fsutil.js +55 -12
- package/src/tools/ignore.js +192 -0
- package/src/tools/index.js +226 -36
- package/src/tools/jobs.js +4 -12
- package/src/tools/outline.js +3 -1
- package/src/tools/pdf.js +1326 -0
- package/src/tools/shell.js +123 -0
- package/src/tools/verify.js +20 -4
- package/src/tools/webfetch.js +18 -5
- package/src/ui/inputbox.js +2 -2
- package/src/ui/screen.js +4 -1
- package/src/ui/status.js +22 -0
package/src/agent/compact.js
CHANGED
|
@@ -1,226 +1,269 @@
|
|
|
1
|
-
// 컨텍스트가 차면 오래된 대화를 '요약해서' 이어 간다.
|
|
2
|
-
//
|
|
3
|
-
// 그냥 잘라내면 방금까지 뭘 하고 있었는지 모델이 잊는다. 파일을 다시 읽고,
|
|
4
|
-
// 이미 고친 곳을 또 고치고, 정해 둔 방침을 어긴다. 그래서 자르는 대신 접는다.
|
|
5
|
-
//
|
|
6
|
-
// 접을 때 지켜야 하는 것 두 가지:
|
|
7
|
-
// 1) 도구 호출과 그 결과는 한 몸이다. 사이를 끊으면 규격 위반이라 서버가 400 을 낸다.
|
|
8
|
-
// 2) 요약도 모델이 만든다. 그 요청이 실패하면 프로그램이 멈추면 안 된다 —
|
|
9
|
-
// 실패하면 옛 방식(그냥 자르기)으로 물러선다.
|
|
10
|
-
import { chat } from '../backend/adapter.js';
|
|
11
|
-
import { estimateTokens, safeCut, safeHead } from './session.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export const
|
|
16
|
-
export const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export const
|
|
38
|
-
export const
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
export
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
session.messages
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
1
|
+
// 컨텍스트가 차면 오래된 대화를 '요약해서' 이어 간다.
|
|
2
|
+
//
|
|
3
|
+
// 그냥 잘라내면 방금까지 뭘 하고 있었는지 모델이 잊는다. 파일을 다시 읽고,
|
|
4
|
+
// 이미 고친 곳을 또 고치고, 정해 둔 방침을 어긴다. 그래서 자르는 대신 접는다.
|
|
5
|
+
//
|
|
6
|
+
// 접을 때 지켜야 하는 것 두 가지:
|
|
7
|
+
// 1) 도구 호출과 그 결과는 한 몸이다. 사이를 끊으면 규격 위반이라 서버가 400 을 낸다.
|
|
8
|
+
// 2) 요약도 모델이 만든다. 그 요청이 실패하면 프로그램이 멈추면 안 된다 —
|
|
9
|
+
// 실패하면 옛 방식(그냥 자르기)으로 물러선다.
|
|
10
|
+
import { chat } from '../backend/adapter.js';
|
|
11
|
+
import { estimateTokens, safeCut, safeHead } from './session.js';
|
|
12
|
+
import { 그림장수, 글만 } from '../backend/vision.js';
|
|
13
|
+
|
|
14
|
+
// 몇 %에서 접기 시작할지. 접고 나서 다시 금방 차면 아무 소용이 없으니 넉넉히 비운다.
|
|
15
|
+
export const COMPACT_AT = 0.8;
|
|
16
|
+
export const KEEP_TAIL_RATIO = 0.3; // 최근 대화 중 남길 비율
|
|
17
|
+
export const KEEP_HEAD = 2; // 맨 처음 요청은 언제나 남긴다 (목표가 거기 있다)
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* ── 그 전에, 도구 결과부터 접는다 ──────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* 요약 압축은 잃는 것이 크다. 사람이 한 말도, 모델이 왜 그렇게 정했는지도
|
|
23
|
+
* 세 줄로 줄어든다. 그런데 자리를 실제로 먹고 있는 것은 대개 그쪽이 아니다.
|
|
24
|
+
*
|
|
25
|
+
* Read 로 600줄짜리 파일을 한 번 열면 32k 창의 15% 가 그 자리에서 사라지고,
|
|
26
|
+
* 그 사본은 대화가 끝날 때까지 이력에 그대로 눌러앉는다. 이미 다 읽고 고친
|
|
27
|
+
* 파일인데도 그렇다. 세 번 읽으면 창의 절반이 옛날 파일 내용이다.
|
|
28
|
+
*
|
|
29
|
+
* 그래서 80% 에서 통째로 요약하기 전에, 먼저 **오래된 도구 결과만** 한 줄로
|
|
30
|
+
* 접는다. 사람 말과 모델의 판단은 한 글자도 안 건드린다. 무엇을 읽었는지는
|
|
31
|
+
* 남기므로 모델은 필요하면 그 파일을 다시 읽으면 된다 — 읽는 값은 싸고,
|
|
32
|
+
* 하던 일을 잊는 값은 비싸다.
|
|
33
|
+
*
|
|
34
|
+
* 최근 것은 안 접는다. 방금 읽은 것을 접으면 그 자리에서 다시 읽게 되고,
|
|
35
|
+
* 그러면 접은 보람이 없다.
|
|
36
|
+
*/
|
|
37
|
+
export const FOLD_AT = 0.55; // 이 아래로는 접을 이유가 없다
|
|
38
|
+
export const KEEP_RECENT = 4; // 최근 도구 결과 이만큼은 원문 그대로 둔다
|
|
39
|
+
export const FOLD_MIN = 300; // 이보다 작으면 접어도 자리가 안 준다 (글자 수)
|
|
40
|
+
export const 접힘표 = '(접힘)';
|
|
41
|
+
|
|
42
|
+
/** 인자에서 사람이 알아볼 한 조각만 뽑는다. 경로가 제일 쓸모 있다. */
|
|
43
|
+
function 어디(args) {
|
|
44
|
+
if (!args || typeof args !== 'object') return '';
|
|
45
|
+
for (const k of ['file_path', 'path', 'pattern', 'command', 'query', 'url', '목적']) {
|
|
46
|
+
const v = args[k];
|
|
47
|
+
if (typeof v === 'string' && v.trim()) return v.trim().replace(/\s+/g, ' ').slice(0, 60);
|
|
48
|
+
}
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* ── 그림은 따로 뺀다 ───────────────────────────────────────────────────
|
|
54
|
+
*
|
|
55
|
+
* 위의 접기는 도구 결과(role:'tool')만 건드린다. 사람 말은 한 글자도 안 건드리는
|
|
56
|
+
* 것이 그 함수의 약속이라 그렇다. 그런데 그림은 **사람 말 자리에** 실린다
|
|
57
|
+
* (backend/vision.js 의 그림메시지 머리말 — 도구 결과에는 못 넣는다).
|
|
58
|
+
*
|
|
59
|
+
* 그림 한 장은 base64 로 몇 MB 다. 화면 사진 몇 장을 이어 보여 주면 대화가
|
|
60
|
+
* 그것만으로 무거워지고, 오래된 사진은 이미 이야기가 끝난 것이다. 그래서
|
|
61
|
+
* 최근 몇 장만 남기고 나머지는 **무엇이었는지만 남기고** 뺀다.
|
|
62
|
+
*
|
|
63
|
+
* 사람이 쓴 말은 그대로 둔다 — 빼는 것은 그림 조각뿐이다.
|
|
64
|
+
*/
|
|
65
|
+
export const KEEP_IMAGES = 2;
|
|
66
|
+
|
|
67
|
+
/** 오래된 그림을 이름만 남기고 뺀다. @returns {{뺀것:number, 뺀것들:Array}} */
|
|
68
|
+
export function foldImages(session, { keep = KEEP_IMAGES } = {}) {
|
|
69
|
+
const ms = session.messages ?? [];
|
|
70
|
+
const 자리 = [];
|
|
71
|
+
ms.forEach((m, i) => { if (그림장수(m)) 자리.push(i); });
|
|
72
|
+
|
|
73
|
+
const 뺄것 = 자리.slice(0, Math.max(0, 자리.length - keep));
|
|
74
|
+
const 뺀것들 = [];
|
|
75
|
+
for (const i of 뺄것) {
|
|
76
|
+
const m = ms[i];
|
|
77
|
+
const 장수 = 그림장수(m);
|
|
78
|
+
const 글 = 글만(m);
|
|
79
|
+
ms[i] = { ...m, content: `${글}
|
|
80
|
+
${접힘표} 그림 ${장수}장은 자리를 비우려고 뺐습니다. 필요하면 다시 여세요.` };
|
|
81
|
+
delete ms[i].images;
|
|
82
|
+
뺀것들.push({ 장수 });
|
|
83
|
+
}
|
|
84
|
+
return { 뺀것: 뺄것.length, 뺀것들 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function shouldFold(session, at = FOLD_AT) {
|
|
88
|
+
const b = session.breakdown();
|
|
89
|
+
return b.total > 0 && b.used / b.total >= at;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 오래된 도구 결과를 한 줄로 접는다.
|
|
94
|
+
*
|
|
95
|
+
* 짝은 안 건드린다 — 메시지를 지우는 게 아니라 **내용만** 바꾸므로 호출과
|
|
96
|
+
* 결과의 짝이 그대로다. 규격이 깨질 자리가 아예 없다.
|
|
97
|
+
*
|
|
98
|
+
* @returns {{접은것: number, 아낀토큰: number}}
|
|
99
|
+
*/
|
|
100
|
+
export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN } = {}) {
|
|
101
|
+
const ms = session.messages ?? [];
|
|
102
|
+
|
|
103
|
+
// 호출 쪽에서 이름과 인자를 가져온다. 결과 메시지에는 그게 안 실려 있다.
|
|
104
|
+
const 이름표 = new Map();
|
|
105
|
+
for (const m of ms) {
|
|
106
|
+
for (const c of m?.tool_calls ?? []) {
|
|
107
|
+
let args = {};
|
|
108
|
+
try { args = JSON.parse(c?.function?.arguments ?? '{}'); } catch { /* 못 읽으면 그만 */ }
|
|
109
|
+
if (c?.id) 이름표.set(c.id, { name: c?.function?.name ?? '도구', args });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const 자리 = [];
|
|
114
|
+
ms.forEach((m, i) => {
|
|
115
|
+
if (m?.role !== 'tool') return;
|
|
116
|
+
const 글 = typeof m.content === 'string' ? m.content : '';
|
|
117
|
+
if (글.startsWith(접힘표)) return; // 이미 접은 것
|
|
118
|
+
자리.push({ i, 글 });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const 접을것 = 자리.slice(0, Math.max(0, 자리.length - keep)).filter((x) => x.글.length >= min);
|
|
122
|
+
let 아낀토큰 = 0;
|
|
123
|
+
// 무엇을 버렸는지 이름으로 남긴다.
|
|
124
|
+
//
|
|
125
|
+
// 조용히 버리면 사람이 모른다 — "아까 그 파일 내용 어디 갔지" 를 스스로 답할
|
|
126
|
+
// 길이 없다. 접힌 자리에 표시가 남긴 하지만 그건 모델이 보는 쪽이고,
|
|
127
|
+
// 사람 화면에는 여기서 돌려주는 목록으로 알려 준다.
|
|
128
|
+
const 접은것들 = [];
|
|
129
|
+
|
|
130
|
+
for (const { i, 글 } of 접을것) {
|
|
131
|
+
const m = ms[i];
|
|
132
|
+
const 아는것 = 이름표.get(m.tool_call_id) ?? { name: m.tool_name ?? '도구', args: {} };
|
|
133
|
+
const 곳 = 어디(아는것.args);
|
|
134
|
+
const 줄수 = 글.split('\n').length;
|
|
135
|
+
const 전 = estimateTokens(글);
|
|
136
|
+
ms[i] = {
|
|
137
|
+
...m,
|
|
138
|
+
content: `${접힘표} ${아는것.name}${곳 ? `(${곳})` : ''} — ${줄수}줄. `
|
|
139
|
+
+ '자리를 비우려고 내용을 접었습니다. 필요하면 다시 읽으세요.',
|
|
140
|
+
};
|
|
141
|
+
const 아낀것 = 전 - estimateTokens(ms[i].content);
|
|
142
|
+
아낀토큰 += 아낀것;
|
|
143
|
+
접은것들.push({ 도구: 아는것.name, 곳, 줄수, 토큰: Math.max(0, 아낀것) });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { 접은것: 접을것.length, 아낀토큰: Math.max(0, 아낀토큰), 접은것들 };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const 요약지시 = `지금까지의 대화를 다음 형식으로 요약하세요. 이어서 일할 사람이 이것만 보고도 계속할 수 있어야 합니다.
|
|
150
|
+
추측하지 말고 실제로 오간 내용만 쓰세요. 한국어로, 각 항목 3줄 이내로 쓰세요.
|
|
151
|
+
|
|
152
|
+
## 목표
|
|
153
|
+
사용자가 무엇을 시켰는가.
|
|
154
|
+
|
|
155
|
+
## 한 일
|
|
156
|
+
어떤 파일을 어떻게 고쳤는가. 파일 경로를 그대로 적으세요.
|
|
157
|
+
|
|
158
|
+
## 알아낸 것
|
|
159
|
+
코드·환경에 대해 확인된 사실. 다시 조사하지 않아도 되도록.
|
|
160
|
+
|
|
161
|
+
## 정한 것
|
|
162
|
+
어떤 방침·이름·구조로 하기로 했는가. 사용자가 거절한 것도 적으세요.
|
|
163
|
+
|
|
164
|
+
## 남은 일
|
|
165
|
+
아직 안 한 것.`;
|
|
166
|
+
|
|
167
|
+
// 자르는 자리를 찾는 두 함수는 session.js 에 있다. 접기와 그냥 줄이기(trim)가
|
|
168
|
+
// **같은 잣대**를 써야 하기 때문이다 — 한쪽만 짝을 맞추면 물러서는 순간 대화가 망가진다.
|
|
169
|
+
// 쓰던 자리가 있으니 이름은 그대로 내보낸다.
|
|
170
|
+
export { safeCut, safeHead } from './session.js';
|
|
171
|
+
|
|
172
|
+
/** 접을 구간과 남길 구간을 나눈다. */
|
|
173
|
+
export function split(messages, { keepHead = KEEP_HEAD, tailRatio = KEEP_TAIL_RATIO } = {}) {
|
|
174
|
+
const n = messages.length;
|
|
175
|
+
const wantTail = Math.max(4, Math.floor(n * tailRatio));
|
|
176
|
+
const cut = safeCut(messages, n - wantTail);
|
|
177
|
+
const head = safeHead(messages, Math.min(keepHead, cut));
|
|
178
|
+
if (cut <= head) return null; // 접을 것이 없다
|
|
179
|
+
const fold = messages.slice(head, cut);
|
|
180
|
+
if (fold.length < 4) return null;
|
|
181
|
+
return { head: messages.slice(0, head), fold, tail: messages.slice(cut) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 요약에 넣을 글. 도구 결과는 길기만 하고 요약에 도움이 안 되니 앞부분만 준다.
|
|
185
|
+
function transcript(msgs) {
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const m of msgs) {
|
|
188
|
+
const who = m.role === 'user' ? '사용자' : m.role === 'assistant' ? '나' : '도구결과';
|
|
189
|
+
let body = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
|
|
190
|
+
if (m.role === 'tool') body = body.slice(0, 400);
|
|
191
|
+
const calls = (m.tool_calls ?? []).map((t) => {
|
|
192
|
+
const fn = t.function ?? t;
|
|
193
|
+
const args = typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments ?? {});
|
|
194
|
+
return `${fn.name}(${args.slice(0, 160)})`;
|
|
195
|
+
});
|
|
196
|
+
if (calls.length) out.push(`${who}: [도구] ${calls.join(', ')}`);
|
|
197
|
+
if (body.trim()) out.push(`${who}: ${body.trim()}`);
|
|
198
|
+
}
|
|
199
|
+
return out.join('\n');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 실제로 접는다.
|
|
204
|
+
* @returns {{ok:boolean, folded:number, before:number, after:number, summary?:string, why?:string}}
|
|
205
|
+
*/
|
|
206
|
+
export async function compact(session, { auto = false, signal = null, onBackoff = null } = {}) {
|
|
207
|
+
const before = session.breakdown().used;
|
|
208
|
+
const parts = split(session.messages);
|
|
209
|
+
if (!parts) return { ok: false, why: '접을 만큼 쌓이지 않았습니다', folded: 0, before, after: before };
|
|
210
|
+
|
|
211
|
+
const text = transcript(parts.fold);
|
|
212
|
+
let summary = null;
|
|
213
|
+
// 서버가 잠깐 막아 다시 부른 횟수. 알림은 부르는 쪽(onBackoff)이 화면에 내고, 셈은 여기서 한다.
|
|
214
|
+
let 다시 = 0;
|
|
215
|
+
const 셈하기 = (n) => { session.usage.retries = (session.usage.retries ?? 0) + Math.max(0, n); };
|
|
216
|
+
try {
|
|
217
|
+
const r = await chat(session.conn, {
|
|
218
|
+
messages: [
|
|
219
|
+
{ role: 'system', content: '너는 대화를 요약한다. 요약 외에 다른 말을 하지 않는다.' },
|
|
220
|
+
{ role: 'user', content: `${요약지시}\n\n----- 대화 -----\n${text}` },
|
|
221
|
+
],
|
|
222
|
+
maxTokens: 1200,
|
|
223
|
+
think: session.conn.kind === 'ollama' ? false : 'low', // 요약에 깊은 사고는 필요 없다
|
|
224
|
+
// 사용자가 Ctrl+C 를 누르면 이것도 멈춰야 한다. 안 받으면 답하는 도중
|
|
225
|
+
// 화면이 멈춰 있고 아무 키도 안 먹는 것처럼 보인다.
|
|
226
|
+
signal,
|
|
227
|
+
onBackoff: (알림) => { 다시 += 1; onBackoff?.(알림); },
|
|
228
|
+
// 요약 하나에 5분을 기다릴 이유가 없다. 그만한 서버면 그냥 줄이는 편이 빠르다.
|
|
229
|
+
timeout: 60000,
|
|
230
|
+
});
|
|
231
|
+
셈하기(다시);
|
|
232
|
+
summary = (r.content ?? '').trim();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
// 사용자가 끊은 것은 실패가 아니다. 대화를 건드리지 않고 그대로 둔다 —
|
|
235
|
+
// 여기서 물러서기(trim)까지 해 버리면, 끊었는데 대화가 줄어 있게 된다.
|
|
236
|
+
if (err?.name === 'Aborted' || signal?.aborted) {
|
|
237
|
+
셈하기(다시 - 1); // 기다리다 끊긴 마지막 것은 다시 부른 것이 아니다
|
|
238
|
+
return { ok: false, aborted: true, why: '중단했습니다', folded: 0, before, after: before };
|
|
239
|
+
}
|
|
240
|
+
셈하기(다시);
|
|
241
|
+
summary = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 요약을 못 받았으면 옛 방식으로 물러선다. 멈추지는 않는다.
|
|
245
|
+
if (!summary) {
|
|
246
|
+
const folded = session.trim();
|
|
247
|
+
const after = session.breakdown().used;
|
|
248
|
+
return { ok: folded > 0, folded, before, after, why: '요약을 받지 못해 그냥 줄였습니다', fallback: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
session.messages = [
|
|
252
|
+
...parts.head,
|
|
253
|
+
{
|
|
254
|
+
role: 'user',
|
|
255
|
+
content: `[앞선 대화 ${parts.fold.length}개를 요약해 접었습니다. 아래가 그 요약입니다.]\n\n${summary}\n\n[요약 끝. 이어서 진행하세요. 파일 내용이 필요하면 다시 읽으세요.]`,
|
|
256
|
+
},
|
|
257
|
+
...parts.tail,
|
|
258
|
+
];
|
|
259
|
+
session.filesRead.clear(); // 접힌 뒤에는 읽어 둔 파일도 기억에서 지운다
|
|
260
|
+
|
|
261
|
+
const after = session.breakdown().used;
|
|
262
|
+
return { ok: true, folded: parts.fold.length, before, after, summary, auto };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 지금 접어야 하는가. */
|
|
266
|
+
export function shouldCompact(session, at = COMPACT_AT) {
|
|
267
|
+
const b = session.breakdown();
|
|
268
|
+
return b.total > 0 && b.used / b.total >= at;
|
|
269
|
+
}
|