deel-local-cli 1.10.0 → 1.12.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.ko.md +1167 -1166
- package/README.md +1224 -1223
- package/bin/deel.js +1 -1
- package/package.json +60 -60
- package/src/acp/serve.js +810 -769
- package/src/agent/budget.js +156 -153
- package/src/agent/compact.js +128 -22
- package/src/agent/effort.js +308 -197
- package/src/agent/evolve.js +263 -213
- package/src/agent/filemem.js +16 -2
- package/src/agent/loop.js +1707 -1516
- package/src/agent/memory.js +156 -152
- package/src/agent/mention.js +210 -210
- package/src/agent/session.js +1049 -948
- package/src/backend/adapter.js +1159 -798
- package/src/backend/cachemark.js +150 -0
- package/src/backend/detect.js +327 -288
- package/src/backend/quota.js +250 -157
- package/src/backend/retry.js +30 -6
- package/src/backend/tokens.js +37 -0
- package/src/backend/toolfit.js +352 -325
- package/src/backend/wire.js +600 -0
- package/src/commands.js +3135 -2998
- package/src/i18n/en.js +544 -520
- package/src/i18n/ja.js +497 -473
- package/src/i18n/ko.js +591 -567
- package/src/i18n/zh.js +497 -473
- package/src/oneshot.js +620 -549
- package/src/providers/bedrock.js +17 -0
- package/src/repl.js +2602 -2515
- package/src/tools/fsutil.js +265 -250
- package/src/tools/verify.js +358 -357
- package/src/ui/pick.js +115 -115
- package/src/ui/status.js +613 -610
package/src/agent/budget.js
CHANGED
|
@@ -1,153 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 상한을 **모델에 맞춰** 정한다.
|
|
3
|
-
*
|
|
4
|
-
* ── 왜 고정 숫자를 버렸나 ───────────────────────────────────────────────
|
|
5
|
-
*
|
|
6
|
-
* 전에는 전부 못 박혀 있었다. 걸음 24회, Read 2,000줄, Glob 200개,
|
|
7
|
-
* WebFetch 20,000자. 그런데 이 값들이 맞는 모델은 **하나도 없다.**
|
|
8
|
-
*
|
|
9
|
-
* 8k 짜리 로컬 모델에 20,000자를 부어 넣으면 그 한 번으로 창이 넘친다.
|
|
10
|
-
* 앞엣말을 잊은 채로 답하니, 사람 눈에는 "모델이 멍청해졌다" 로 보인다.
|
|
11
|
-
*
|
|
12
|
-
* 655k 짜리 모델은 24걸음 만에 끊긴다. 대시보드 하나를 만들려면 폴더를
|
|
13
|
-
* 잡고 파일 몇 개를 만들고 돌려 보고 고쳐야 하는데, 만들다 만 채로 끝난다.
|
|
14
|
-
* 여유가 96% 남아 있는데도 그렇다.
|
|
15
|
-
*
|
|
16
|
-
* 같은 숫자가 한쪽에선 너무 크고 다른 쪽에선 너무 작다. 그러면 그건 숫자를
|
|
17
|
-
* 잘못 고른 게 아니라 **고정한 것 자체가 틀린 것**이다.
|
|
18
|
-
*
|
|
19
|
-
* 그래서 전부 컨텍스트에서 뽑아 쓴다. 큰 모델에는 크게 주고 작은 모델에는
|
|
20
|
-
* 작게 준다. 사람이 `/ctx` 로 컨텍스트를 다시 재면 이 값들도 같이 따라온다.
|
|
21
|
-
*
|
|
22
|
-
* ── 걸음 수는 목표가 아니라 마지막 안전장치다 ──────────────────────────
|
|
23
|
-
*
|
|
24
|
-
* 헛도는 것은 걸음 수로 못 잡는다 — 그건 '잘 되고 있는 긴 작업' 과 '헛도는
|
|
25
|
-
* 작업' 을 구분 못 한다. 헛도는 것은 같은 자리를 되풀이하는지로 잡고
|
|
26
|
-
* (loop.js), 컨텍스트가 차면 접는다(compact.js). 여기 값은 그 둘이 다 놓쳤을
|
|
27
|
-
* 때 끝없이 도는 것을 막는 **마지막 울타리**다. 그러니 넉넉해도 된다.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
// 컨텍스트 1k 당 몇 걸음을 줄까. 일의 성격마다 다르다 —
|
|
31
|
-
// '묻기' 는 설명 한 번이면 끝이고, '총괄' 은 여러 갈래를 끝까지 끌고 간다.
|
|
32
|
-
const 걸음규칙 = {
|
|
33
|
-
ask: { 배수: 0.25, 최소: 4, 최대: 12 },
|
|
34
|
-
plan: { 배수: 0.6, 최소: 8, 최대: 40 },
|
|
35
|
-
architect: { 배수: 0.7, 최소: 8, 최대: 48 },
|
|
36
|
-
auto: { 배수: 1.5, 최소: 16, 최대: 200 },
|
|
37
|
-
code: { 배수: 1.5, 최소: 16, 최대: 200 },
|
|
38
|
-
debug: { 배수: 2, 최소: 16, 최대: 250 },
|
|
39
|
-
orchestrator: { 배수: 3, 최소: 24, 최대: 400 },
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const 사이 = (v, 최소, 최대) => Math.max(최소, Math.min(최대, Math.round(v)));
|
|
43
|
-
|
|
44
|
-
/** 컨텍스트가 아직 안 잡혔을 때 쓰는 값. 못 알아냈다는 뜻이지 정답이 아니다. */
|
|
45
|
-
const 모를때 = 32768;
|
|
46
|
-
|
|
47
|
-
const k = (ctx) => (Number(ctx) > 0 ? Number(ctx) : 모를때) / 1024;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 이 모드·이 모델에서 한 턴에 몇 걸음까지 돌까.
|
|
51
|
-
*
|
|
52
|
-
* @param {string} 모드id auto · code · plan …
|
|
53
|
-
* @param {number} ctx 모델 컨텍스트 길이(토큰)
|
|
54
|
-
*/
|
|
55
|
-
export function 걸음수(모드id, ctx) {
|
|
56
|
-
const r = 걸음규칙[모드id] ?? 걸음규칙.auto;
|
|
57
|
-
return 사이(k(ctx) * r.배수, r.최소, r.최대);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 한 번에 읽을 파일 줄 수.
|
|
62
|
-
*
|
|
63
|
-
* 8k 모델에 2,000줄을 부어 넣으면 그 한 번으로 창이 넘친다. 넘치면 접히고,
|
|
64
|
-
* 접히면 앞엣말을 잊는다 — 그런데 화면에는 `2,000줄` 이라고만 떠서 사람은
|
|
65
|
-
* 잘 읽은 줄 안다.
|
|
66
|
-
*/
|
|
67
|
-
export function 읽을줄수(ctx) {
|
|
68
|
-
return 사이(k(ctx) * 12, 200, 4000);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Glob 이 돌려줄 파일 개수. 목록은 한 줄에 한 개라 줄 수와 성격이 같다. */
|
|
72
|
-
export function 찾을개수(ctx) {
|
|
73
|
-
return 사이(k(ctx) * 6, 50, 1000);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Grep 이 돌려줄 줄 수. */
|
|
77
|
-
export function 찾을줄수(ctx) {
|
|
78
|
-
return 사이(k(ctx) * 8, 80, 1500);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Outline 이 돌려줄 줄 수.
|
|
83
|
-
*
|
|
84
|
-
* Grep 보다 넉넉히 준다. 뼈대 한 줄은 ` 42 fn 접어쓰기` 처럼 짧은데,
|
|
85
|
-
* 그 한 줄이 파일 하나를 통째로 안 읽어도 되게 해 준다 — 줄당 값이 제일 크다.
|
|
86
|
-
* 8k 에서 120줄이면 파일 열댓 개의 모양이 들어온다. 그 정도면 "어디를 고칠지"
|
|
87
|
-
* 는 정해진다.
|
|
88
|
-
*/
|
|
89
|
-
export function 뼈대줄수(ctx) {
|
|
90
|
-
return 사이(k(ctx) * 15, 120, 2500);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* WebFetch 가 한 번에 가져올 글자 수.
|
|
95
|
-
*
|
|
96
|
-
* 토큰이 아니라 **글자** 다. 영문 JSON 은 3.6자에 1토큰이지만 한글은 1자에
|
|
97
|
-
* 1토큰이라, 같은 글자 수라도 한글이 세 배 넘게 무겁다. 무거운 쪽에 맞춘다 —
|
|
98
|
-
* 넘쳐서 잊는 것보다 모자라서 한 번 더 부르는 편이 낫다.
|
|
99
|
-
*/
|
|
100
|
-
export function 웹글자수(ctx) {
|
|
101
|
-
return 사이(k(ctx) * 400, 4000, 120000);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 하위 작업이 돌 걸음 수.
|
|
106
|
-
*
|
|
107
|
-
* 부모보다 적게 준다. 하위 작업은 '한 덩이' 를 맡는 것이지 전체를 맡는 게
|
|
108
|
-
* 아니다. 부모만큼 주면 하위가 부모 일을 통째로 다시 하려 들고, 그러면
|
|
109
|
-
* 창을 나눈 뜻이 없어진다. 대신 너무 적으면 맡긴 덩이도 못 끝내니
|
|
110
|
-
* 8걸음은 무슨 일이 있어도 준다.
|
|
111
|
-
*/
|
|
112
|
-
export function 하위걸음수(모드id, ctx) {
|
|
113
|
-
return Math.max(8, Math.round(걸음수(모드id, ctx) / 2));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 하위 작업이 부모에게 돌려줄 요약의 길이(글자).
|
|
118
|
-
*
|
|
119
|
-
* 이게 하위 작업의 **존재 이유**다. 하위가 파일 열 개를 읽고 만들어도 부모
|
|
120
|
-
* 창에 들어가는 것은 이만큼뿐이다. 그러니 부모 창에 맞춰야지, 하위가 한 일의
|
|
121
|
-
* 양에 맞추면 안 된다 — 그러면 나눈 것이 도로 합쳐진다.
|
|
122
|
-
*
|
|
123
|
-
* 부모 창의 약 5% 를 넘지 않게 잡는다. 8k 면 400자, 655k 면 4,000자다.
|
|
124
|
-
*/
|
|
125
|
-
export function 요약길이(ctx) {
|
|
126
|
-
return 사이(k(ctx) * 50, 400, 4000);
|
|
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
|
-
return
|
|
152
|
-
|
|
153
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 상한을 **모델에 맞춰** 정한다.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 고정 숫자를 버렸나 ───────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 전에는 전부 못 박혀 있었다. 걸음 24회, Read 2,000줄, Glob 200개,
|
|
7
|
+
* WebFetch 20,000자. 그런데 이 값들이 맞는 모델은 **하나도 없다.**
|
|
8
|
+
*
|
|
9
|
+
* 8k 짜리 로컬 모델에 20,000자를 부어 넣으면 그 한 번으로 창이 넘친다.
|
|
10
|
+
* 앞엣말을 잊은 채로 답하니, 사람 눈에는 "모델이 멍청해졌다" 로 보인다.
|
|
11
|
+
*
|
|
12
|
+
* 655k 짜리 모델은 24걸음 만에 끊긴다. 대시보드 하나를 만들려면 폴더를
|
|
13
|
+
* 잡고 파일 몇 개를 만들고 돌려 보고 고쳐야 하는데, 만들다 만 채로 끝난다.
|
|
14
|
+
* 여유가 96% 남아 있는데도 그렇다.
|
|
15
|
+
*
|
|
16
|
+
* 같은 숫자가 한쪽에선 너무 크고 다른 쪽에선 너무 작다. 그러면 그건 숫자를
|
|
17
|
+
* 잘못 고른 게 아니라 **고정한 것 자체가 틀린 것**이다.
|
|
18
|
+
*
|
|
19
|
+
* 그래서 전부 컨텍스트에서 뽑아 쓴다. 큰 모델에는 크게 주고 작은 모델에는
|
|
20
|
+
* 작게 준다. 사람이 `/ctx` 로 컨텍스트를 다시 재면 이 값들도 같이 따라온다.
|
|
21
|
+
*
|
|
22
|
+
* ── 걸음 수는 목표가 아니라 마지막 안전장치다 ──────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* 헛도는 것은 걸음 수로 못 잡는다 — 그건 '잘 되고 있는 긴 작업' 과 '헛도는
|
|
25
|
+
* 작업' 을 구분 못 한다. 헛도는 것은 같은 자리를 되풀이하는지로 잡고
|
|
26
|
+
* (loop.js), 컨텍스트가 차면 접는다(compact.js). 여기 값은 그 둘이 다 놓쳤을
|
|
27
|
+
* 때 끝없이 도는 것을 막는 **마지막 울타리**다. 그러니 넉넉해도 된다.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// 컨텍스트 1k 당 몇 걸음을 줄까. 일의 성격마다 다르다 —
|
|
31
|
+
// '묻기' 는 설명 한 번이면 끝이고, '총괄' 은 여러 갈래를 끝까지 끌고 간다.
|
|
32
|
+
const 걸음규칙 = {
|
|
33
|
+
ask: { 배수: 0.25, 최소: 4, 최대: 12 },
|
|
34
|
+
plan: { 배수: 0.6, 최소: 8, 최대: 40 },
|
|
35
|
+
architect: { 배수: 0.7, 최소: 8, 최대: 48 },
|
|
36
|
+
auto: { 배수: 1.5, 최소: 16, 최대: 200 },
|
|
37
|
+
code: { 배수: 1.5, 최소: 16, 최대: 200 },
|
|
38
|
+
debug: { 배수: 2, 최소: 16, 최대: 250 },
|
|
39
|
+
orchestrator: { 배수: 3, 최소: 24, 최대: 400 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const 사이 = (v, 최소, 최대) => Math.max(최소, Math.min(최대, Math.round(v)));
|
|
43
|
+
|
|
44
|
+
/** 컨텍스트가 아직 안 잡혔을 때 쓰는 값. 못 알아냈다는 뜻이지 정답이 아니다. */
|
|
45
|
+
const 모를때 = 32768;
|
|
46
|
+
|
|
47
|
+
const k = (ctx) => (Number(ctx) > 0 ? Number(ctx) : 모를때) / 1024;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 이 모드·이 모델에서 한 턴에 몇 걸음까지 돌까.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} 모드id auto · code · plan …
|
|
53
|
+
* @param {number} ctx 모델 컨텍스트 길이(토큰)
|
|
54
|
+
*/
|
|
55
|
+
export function 걸음수(모드id, ctx) {
|
|
56
|
+
const r = 걸음규칙[모드id] ?? 걸음규칙.auto;
|
|
57
|
+
return 사이(k(ctx) * r.배수, r.최소, r.최대);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 한 번에 읽을 파일 줄 수.
|
|
62
|
+
*
|
|
63
|
+
* 8k 모델에 2,000줄을 부어 넣으면 그 한 번으로 창이 넘친다. 넘치면 접히고,
|
|
64
|
+
* 접히면 앞엣말을 잊는다 — 그런데 화면에는 `2,000줄` 이라고만 떠서 사람은
|
|
65
|
+
* 잘 읽은 줄 안다.
|
|
66
|
+
*/
|
|
67
|
+
export function 읽을줄수(ctx) {
|
|
68
|
+
return 사이(k(ctx) * 12, 200, 4000);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Glob 이 돌려줄 파일 개수. 목록은 한 줄에 한 개라 줄 수와 성격이 같다. */
|
|
72
|
+
export function 찾을개수(ctx) {
|
|
73
|
+
return 사이(k(ctx) * 6, 50, 1000);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Grep 이 돌려줄 줄 수. */
|
|
77
|
+
export function 찾을줄수(ctx) {
|
|
78
|
+
return 사이(k(ctx) * 8, 80, 1500);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Outline 이 돌려줄 줄 수.
|
|
83
|
+
*
|
|
84
|
+
* Grep 보다 넉넉히 준다. 뼈대 한 줄은 ` 42 fn 접어쓰기` 처럼 짧은데,
|
|
85
|
+
* 그 한 줄이 파일 하나를 통째로 안 읽어도 되게 해 준다 — 줄당 값이 제일 크다.
|
|
86
|
+
* 8k 에서 120줄이면 파일 열댓 개의 모양이 들어온다. 그 정도면 "어디를 고칠지"
|
|
87
|
+
* 는 정해진다.
|
|
88
|
+
*/
|
|
89
|
+
export function 뼈대줄수(ctx) {
|
|
90
|
+
return 사이(k(ctx) * 15, 120, 2500);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* WebFetch 가 한 번에 가져올 글자 수.
|
|
95
|
+
*
|
|
96
|
+
* 토큰이 아니라 **글자** 다. 영문 JSON 은 3.6자에 1토큰이지만 한글은 1자에
|
|
97
|
+
* 1토큰이라, 같은 글자 수라도 한글이 세 배 넘게 무겁다. 무거운 쪽에 맞춘다 —
|
|
98
|
+
* 넘쳐서 잊는 것보다 모자라서 한 번 더 부르는 편이 낫다.
|
|
99
|
+
*/
|
|
100
|
+
export function 웹글자수(ctx) {
|
|
101
|
+
return 사이(k(ctx) * 400, 4000, 120000);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 하위 작업이 돌 걸음 수.
|
|
106
|
+
*
|
|
107
|
+
* 부모보다 적게 준다. 하위 작업은 '한 덩이' 를 맡는 것이지 전체를 맡는 게
|
|
108
|
+
* 아니다. 부모만큼 주면 하위가 부모 일을 통째로 다시 하려 들고, 그러면
|
|
109
|
+
* 창을 나눈 뜻이 없어진다. 대신 너무 적으면 맡긴 덩이도 못 끝내니
|
|
110
|
+
* 8걸음은 무슨 일이 있어도 준다.
|
|
111
|
+
*/
|
|
112
|
+
export function 하위걸음수(모드id, ctx) {
|
|
113
|
+
return Math.max(8, Math.round(걸음수(모드id, ctx) / 2));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 하위 작업이 부모에게 돌려줄 요약의 길이(글자).
|
|
118
|
+
*
|
|
119
|
+
* 이게 하위 작업의 **존재 이유**다. 하위가 파일 열 개를 읽고 만들어도 부모
|
|
120
|
+
* 창에 들어가는 것은 이만큼뿐이다. 그러니 부모 창에 맞춰야지, 하위가 한 일의
|
|
121
|
+
* 양에 맞추면 안 된다 — 그러면 나눈 것이 도로 합쳐진다.
|
|
122
|
+
*
|
|
123
|
+
* 부모 창의 약 5% 를 넘지 않게 잡는다. 8k 면 400자, 655k 면 4,000자다.
|
|
124
|
+
*/
|
|
125
|
+
export function 요약길이(ctx) {
|
|
126
|
+
return 사이(k(ctx) * 50, 400, 4000);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 도구 설명을 몇 글자까지 실을까.
|
|
131
|
+
*
|
|
132
|
+
* ── 이게 왜 필요한가 ─────────────────────────────────────────────────
|
|
133
|
+
*
|
|
134
|
+
* 도구 정의는 **매 요청에 통째로 다시 나간다.** 접기로는 못 줄이는 몫이다.
|
|
135
|
+
* 도구가 스무 종이 되면서 코드 모드에서만 그만큼이 매번 실린다. 시스템
|
|
136
|
+
* 프롬프트까지 합치면 8k 모델은 대화 한 줄 전에 자리의 **절반**이 찬다.
|
|
137
|
+
*
|
|
138
|
+
* 여기에 잰 값을 박아 두지 않는다. 박아 두면 도구가 늘 때마다 낡고, 낡은
|
|
139
|
+
* 숫자는 없는 숫자보다 나쁘다 — 그걸 믿고 예산을 짜기 때문이다.
|
|
140
|
+
*
|
|
141
|
+
* 그렇다고 도구를 뺄 수는 없다. 빼면 작은 모델만 할 수 있는 일이 달라져서,
|
|
142
|
+
* "환경마다 다르게 동작" 하게 된다 — 사용자가 피하려는 바로 그것이다.
|
|
143
|
+
* 그래서 **도구는 다 주고 설명만 줄인다.** 이름과 인자는 그대로 두므로
|
|
144
|
+
* 할 수 있는 일은 똑같고, 없어지는 것은 '왜 이걸 쓰는지' 하는 설득뿐이다.
|
|
145
|
+
*
|
|
146
|
+
* 큰 창에서는 안 줄인다(Infinity). 그 설득이 실제로 값을 하기 때문이다 —
|
|
147
|
+
* 모델이 Outline 을 Read 앞에 부르게 만드는 것은 그 두 문장이다.
|
|
148
|
+
*/
|
|
149
|
+
export function 설명길이(ctx) {
|
|
150
|
+
const n = Number(ctx) > 0 ? Number(ctx) : 모를때;
|
|
151
|
+
if (n >= 64000) return Infinity; // 자리가 넉넉하다. 그대로 준다
|
|
152
|
+
if (n >= 24000) return 220;
|
|
153
|
+
if (n >= 12000) return 140;
|
|
154
|
+
return 90; // 8k 언저리. 첫 문장 하나만 남는다
|
|
155
|
+
}
|
|
156
|
+
|
package/src/agent/compact.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// 1) 도구 호출과 그 결과는 한 몸이다. 사이를 끊으면 규격 위반이라 서버가 400 을 낸다.
|
|
8
8
|
// 2) 요약도 모델이 만든다. 그 요청이 실패하면 프로그램이 멈추면 안 된다 —
|
|
9
9
|
// 실패하면 옛 방식(그냥 자르기)으로 물러선다.
|
|
10
|
-
import { chat } from '../backend/adapter.js';
|
|
10
|
+
import { chat, 부른것들, 결과들, 도구결과인가 } from '../backend/adapter.js';
|
|
11
11
|
import { estimateTokens, safeCut, safeHead, 못박을것 } from './session.js';
|
|
12
|
+
import { wasCut } from './effort.js';
|
|
12
13
|
import { 그림장수, 글만 } from '../backend/vision.js';
|
|
13
14
|
|
|
14
15
|
// 몇 %에서 접기 시작할지. 접고 나서 다시 금방 차면 아무 소용이 없으니 넉넉히 비운다.
|
|
@@ -39,6 +40,23 @@ export const KEEP_RECENT = 4; // 최근 도구 결과 이만큼은 원문
|
|
|
39
40
|
export const FOLD_MIN = 300; // 이보다 작으면 접어도 자리가 안 준다 (글자 수)
|
|
40
41
|
export const 접힘표 = '(접힘)';
|
|
41
42
|
|
|
43
|
+
/*
|
|
44
|
+
* ── 접기는 **묶음으로** 한다 ────────────────────────────────────────────
|
|
45
|
+
*
|
|
46
|
+
* 여태 이 함수는 걸음마다 「최근 넷 빼고 다」 를 접었다. 얼핏 맞는 말인데,
|
|
47
|
+
* 걸음마다 도구 결과가 하나씩 늘어나므로 실제로 하는 일은
|
|
48
|
+
* **걸음마다 딱 하나씩 접는 것**이었다.
|
|
49
|
+
*
|
|
50
|
+
* 접는다는 것은 이력 가운데 있는 글을 **딴 글로 바꿔치는 것**이다. 그러면
|
|
51
|
+
* 그 자리부터 뒤가 전부 새 글이 되고, 프리픽스 캐시는 거기서 끊긴다.
|
|
52
|
+
* 즉 걸음마다 한 줄을 아끼려고 **걸음마다 대화 전체를 다시 보내고 있었다.**
|
|
53
|
+
* 아끼려던 것보다 훨씬 크게 쓴 셈이다.
|
|
54
|
+
*
|
|
55
|
+
* 그래서 문턱을 둔다 — 이만큼 비울 수 있을 때만 접는다. 그러면 접기가
|
|
56
|
+
* 가끔 크게 일어나고, 그 사이에는 앞머리가 가만히 있는다.
|
|
57
|
+
*/
|
|
58
|
+
export const 최소이득 = 2000;
|
|
59
|
+
|
|
42
60
|
/** 인자에서 사람이 알아볼 한 조각만 뽑는다. 경로가 제일 쓸모 있다. */
|
|
43
61
|
function 어디(args) {
|
|
44
62
|
if (!args || typeof args !== 'object') return '';
|
|
@@ -97,25 +115,32 @@ export function shouldFold(session, at = FOLD_AT) {
|
|
|
97
115
|
*
|
|
98
116
|
* @returns {{접은것: number, 아낀토큰: number}}
|
|
99
117
|
*/
|
|
100
|
-
export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN } = {}) {
|
|
118
|
+
export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN, 이득문턱 = 최소이득 } = {}) {
|
|
101
119
|
const ms = session.messages ?? [];
|
|
102
120
|
|
|
103
|
-
|
|
121
|
+
/*
|
|
122
|
+
* 호출 쪽에서 이름과 인자를 가져온다. 결과 메시지에는 그게 안 실려 있다.
|
|
123
|
+
*
|
|
124
|
+
* 부름도 결과도 규격마다 다른 자리에 담긴다(backend/adapter.js). 여기가
|
|
125
|
+
* `tool_calls` 와 `role:'tool'` 만 보던 동안 Anthropic 창구에서는 **접은 것이
|
|
126
|
+
* 늘 0 이었다** — 55% 에서 싸게 자리를 비우는 단계가 통째로 죽어 있었고,
|
|
127
|
+
* 그대로 80% 의 손실 있는 요약으로 직행했다. 캐시가 제일 중요한 창구에서
|
|
128
|
+
* 캐시를 지키려고 만든 단계가 안 돈 것이다.
|
|
129
|
+
*/
|
|
104
130
|
const 이름표 = new Map();
|
|
105
131
|
for (const m of ms) {
|
|
106
|
-
for (const c of m
|
|
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
|
-
}
|
|
132
|
+
for (const c of 부른것들(m)) if (c.id) 이름표.set(c.id, { name: c.name ?? '도구', args: c.args ?? {} });
|
|
111
133
|
}
|
|
112
134
|
|
|
113
135
|
const 자리 = [];
|
|
114
136
|
ms.forEach((m, i) => {
|
|
115
|
-
if (m
|
|
116
|
-
const
|
|
137
|
+
if (!도구결과인가(m)) return;
|
|
138
|
+
const 것들 = 결과들(m);
|
|
139
|
+
// 한 메시지에 결과가 여럿 실리는 규격이 있다. 그때는 이어 붙여 한 자리로 본다.
|
|
140
|
+
const 글 = 것들.map((x) => x.글).join('\n');
|
|
117
141
|
if (글.startsWith(접힘표)) return; // 이미 접은 것
|
|
118
|
-
|
|
142
|
+
const 첫 = 것들[0];
|
|
143
|
+
자리.push({ i, 글, 이름: 이름표.get(첫?.id)?.name ?? 첫?.name ?? '' });
|
|
119
144
|
});
|
|
120
145
|
|
|
121
146
|
/*
|
|
@@ -137,6 +162,19 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN }
|
|
|
137
162
|
const 접을것 = 자리
|
|
138
163
|
.slice(0, Math.max(0, 자리.length - keep))
|
|
139
164
|
.filter((x) => x.글.length >= min && x.i !== 지킬할일);
|
|
165
|
+
|
|
166
|
+
/*
|
|
167
|
+
* 비울 수 있는 양이 문턱에 못 미치면 **이번에는 안 접는다.**
|
|
168
|
+
*
|
|
169
|
+
* 한 줄 아끼자고 앞머리를 깨면 그 걸음에 대화가 통째로 다시 나간다.
|
|
170
|
+
* 모아 두었다가 한 번에 접는 편이 눈에 띄게 싸다. 자리가 정말 모자라면
|
|
171
|
+
* 80% 에서 요약 압축이 받아 준다 — 안전망은 그쪽에 있다.
|
|
172
|
+
*/
|
|
173
|
+
const 예상이득 = 접을것.reduce((a, x) => a + estimateTokens(x.글), 0);
|
|
174
|
+
if (예상이득 < 이득문턱) {
|
|
175
|
+
return { 접은것: 0, 아낀토큰: 0, 접은것들: [], 미룸: true, 모인것: 예상이득 };
|
|
176
|
+
}
|
|
177
|
+
|
|
140
178
|
let 아낀토큰 = 0;
|
|
141
179
|
// 무엇을 버렸는지 이름으로 남긴다.
|
|
142
180
|
//
|
|
@@ -158,12 +196,45 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN }
|
|
|
158
196
|
};
|
|
159
197
|
const 아낀것 = 전 - estimateTokens(ms[i].content);
|
|
160
198
|
아낀토큰 += 아낀것;
|
|
161
|
-
|
|
199
|
+
/*
|
|
200
|
+
* 자른 표시용 이름(곳)과 **진짜 경로**를 따로 담는다.
|
|
201
|
+
*
|
|
202
|
+
* 곳 은 화면에 쓰려고 60자에서 자른 것이라, 이걸로 파일 기억을 지우면
|
|
203
|
+
* 긴 경로가 안 맞아서 조용히 안 지워진다 — 그러면 접힌 파일을 다시
|
|
204
|
+
* 읽을 때 「앞에서 읽은 그대로입니다」 만 돌아간다(agent/filemem.js).
|
|
205
|
+
*/
|
|
206
|
+
const 경로 = typeof 아는것.args?.file_path === 'string'
|
|
207
|
+
? 아는것.args.file_path
|
|
208
|
+
: (typeof 아는것.args?.path === 'string' ? 아는것.args.path : null);
|
|
209
|
+
접은것들.push({ 도구: 아는것.name, 곳, 경로, 줄수, 토큰: Math.max(0, 아낀것) });
|
|
162
210
|
}
|
|
163
211
|
|
|
164
212
|
return { 접은것: 접을것.length, 아낀토큰: Math.max(0, 아낀토큰), 접은것들 };
|
|
165
213
|
}
|
|
166
214
|
|
|
215
|
+
/**
|
|
216
|
+
* 접힌 파일을 **파일 기억에서 지울 때 쓸 열쇠** (agent/filemem.js).
|
|
217
|
+
*
|
|
218
|
+
* 접은 자리에 남은 경로는 모델이 적어 보낸 글자 그대로다 — 대개 `src/a.js`
|
|
219
|
+
* 같은 상대 경로다. 그런데 파일 기억은 `ctx.scope.resolve()` 를 거친 **절대
|
|
220
|
+
* 경로**로 묶여 있다(tools/index.js 의 읽은것). 그대로 지우면 Map 이 안 맞아서
|
|
221
|
+
* 조용히 아무것도 안 지워지고, 그러면 접혀 사라진 파일을 다시 읽을 때
|
|
222
|
+
* 「앞에서 읽은 그대로입니다」 가 돌아간다. 모델은 대화 어디에도 없는 글을
|
|
223
|
+
* 가리키는 쪽지를 받고, 결국 또 읽는다 — 접기로 아낀 것을 되읽기로 도로 쓴다.
|
|
224
|
+
*
|
|
225
|
+
* 울타리 밖이라 못 풀면 적힌 그대로 돌려준다. 둘 다 지워 보는 것은 부르는
|
|
226
|
+
* 쪽 몫이다 — 지우는 일은 없는 열쇠에도 안전하다.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} 경로 접은 자리에 적혀 있던 경로 (모델이 준 그대로)
|
|
229
|
+
* @param {object|null} scope safety/guard.js 의 울타리
|
|
230
|
+
* @returns {string} 지울 때 쓸 열쇠
|
|
231
|
+
*/
|
|
232
|
+
export function 접힌파일열쇠(경로, scope = null) {
|
|
233
|
+
const 적힌것 = String(경로 ?? '');
|
|
234
|
+
if (!적힌것) return 적힌것;
|
|
235
|
+
try { return scope?.resolve?.(적힌것) ?? 적힌것; } catch { return 적힌것; }
|
|
236
|
+
}
|
|
237
|
+
|
|
167
238
|
const 요약지시 = `지금까지의 대화를 다음 형식으로 요약하세요. 이어서 일할 사람이 이것만 보고도 계속할 수 있어야 합니다.
|
|
168
239
|
추측하지 말고 실제로 오간 내용만 쓰세요. 한국어로, 각 항목 3줄 이내로 쓰세요.
|
|
169
240
|
|
|
@@ -210,14 +281,20 @@ export function split(messages, { keepHead = KEEP_HEAD, tailRatio = KEEP_TAIL_RA
|
|
|
210
281
|
function transcript(msgs) {
|
|
211
282
|
const out = [];
|
|
212
283
|
for (const m of msgs) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
284
|
+
/*
|
|
285
|
+
* 누가 한 말인지도 규격을 봐야 안다. Anthropic 은 도구 결과를 **사람
|
|
286
|
+
* 차례**에 싣기 때문에, role 만 보면 도구가 뱉은 것이 전부 「사용자」 가
|
|
287
|
+
* 된다. 그러면 아래 400자 자르기도 안 걸려서, 요약을 시키는 요청에 도구
|
|
288
|
+
* 출력이 통째로 실려 나간다 — 요약하려는 대화보다 요약 요청이 더 커진다.
|
|
289
|
+
* 그리고 그렇게 만든 요약이 대화를 **대신하게** 된다.
|
|
290
|
+
*/
|
|
291
|
+
const 결과인가 = 도구결과인가(m);
|
|
292
|
+
const who = 결과인가 ? '도구결과' : (m.role === 'user' ? '사용자' : m.role === 'assistant' ? '나' : '도구결과');
|
|
293
|
+
let body = 결과인가
|
|
294
|
+
? 결과들(m).map((x) => x.글).join('\n')
|
|
295
|
+
: (typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''));
|
|
296
|
+
if (결과인가) body = body.slice(0, 400);
|
|
297
|
+
const calls = 부른것들(m).map((t) => `${t.name}(${JSON.stringify(t.args ?? {}).slice(0, 160)})`);
|
|
221
298
|
if (calls.length) out.push(`${who}: [도구] ${calls.join(', ')}`);
|
|
222
299
|
if (body.trim()) out.push(`${who}: ${body.trim()}`);
|
|
223
300
|
}
|
|
@@ -235,6 +312,8 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
235
312
|
|
|
236
313
|
const text = transcript(parts.fold);
|
|
237
314
|
let summary = null;
|
|
315
|
+
// 왜 못 받았는지. 「못 받았다」 만으로는 사람이 할 수 있는 일이 없다.
|
|
316
|
+
let 못한까닭 = null;
|
|
238
317
|
// 서버가 잠깐 막아 다시 부른 횟수. 알림은 부르는 쪽(onBackoff)이 화면에 내고, 셈은 여기서 한다.
|
|
239
318
|
let 다시 = 0;
|
|
240
319
|
const 셈하기 = (n) => { session.usage.retries = (session.usage.retries ?? 0) + Math.max(0, n); };
|
|
@@ -254,7 +333,20 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
254
333
|
timeout: 60000,
|
|
255
334
|
});
|
|
256
335
|
셈하기(다시);
|
|
257
|
-
|
|
336
|
+
/*
|
|
337
|
+
* 받은 것을 그대로 믿지 않는다. 이 요약은 **대화를 대신하게 될 글**이라,
|
|
338
|
+
* 반쪽이거나 거절이면 대화 전체가 그 반쪽으로 바뀐다.
|
|
339
|
+
*
|
|
340
|
+
* · 상한에 걸려 잘린 요약 — 비어 있지 않으니 그냥 통과했다. 그러면
|
|
341
|
+
* 대화가 문장 중간에서 끊긴 한 토막으로 바뀐다.
|
|
342
|
+
* · 거절 — 이 규격은 거절 글을 `content` 에 담는다(adapter.js). 그러면
|
|
343
|
+
* 대화 전체가 「그건 도와드릴 수 없습니다」 한 줄이 된다.
|
|
344
|
+
*
|
|
345
|
+
* 둘 다 `ok: true` 로 보고됐다. 그냥 줄이는 편이 훨씬 낫다.
|
|
346
|
+
*/
|
|
347
|
+
if (r?.거절) 못한까닭 = '요약을 거절당했습니다';
|
|
348
|
+
else if (wasCut(r)) 못한까닭 = '요약이 상한에서 잘렸습니다';
|
|
349
|
+
else summary = (r.content ?? '').trim();
|
|
258
350
|
} catch (err) {
|
|
259
351
|
// 사용자가 끊은 것은 실패가 아니다. 대화를 건드리지 않고 그대로 둔다 —
|
|
260
352
|
// 여기서 물러서기(trim)까지 해 버리면, 끊었는데 대화가 줄어 있게 된다.
|
|
@@ -263,6 +355,17 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
263
355
|
return { ok: false, aborted: true, why: '중단했습니다', folded: 0, before, after: before };
|
|
264
356
|
}
|
|
265
357
|
셈하기(다시);
|
|
358
|
+
/*
|
|
359
|
+
* 서버가 한 말을 **버리지 않는다.**
|
|
360
|
+
*
|
|
361
|
+
* 여태 `err` 를 통째로 흘렸다. 열쇠가 만료돼 401 이든, 게이트웨이가
|
|
362
|
+
* 모르는 칸에 400 을 내든, 모델 이름이 틀렸든 화면에는 늘 「요약을 받지
|
|
363
|
+
* 못해 그냥 줄였습니다」 한 줄이었다. 그러면 사람은 압축이 왜 매번
|
|
364
|
+
* 물러서는지 알 길이 없고, 고칠 데는 대개 한 줄이면 되는 것이었다.
|
|
365
|
+
*/
|
|
366
|
+
못한까닭 = err?.serverMessage
|
|
367
|
+
? `요약을 못 받았습니다 — ${String(err.serverMessage).replace(/\s+/g, ' ').trim().slice(0, 120)}`
|
|
368
|
+
: `요약을 못 받았습니다 — ${String(err?.message ?? err).slice(0, 120)}`;
|
|
266
369
|
summary = null;
|
|
267
370
|
}
|
|
268
371
|
|
|
@@ -280,7 +383,10 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
280
383
|
*/
|
|
281
384
|
if (folded > 0) session.파일기억?.잊기();
|
|
282
385
|
const after = session.breakdown().used;
|
|
283
|
-
return {
|
|
386
|
+
return {
|
|
387
|
+
ok: folded > 0, folded, before, after, fallback: true,
|
|
388
|
+
why: 못한까닭 ? `${못한까닭}. 그냥 줄였습니다` : '요약을 받지 못해 그냥 줄였습니다',
|
|
389
|
+
};
|
|
284
390
|
}
|
|
285
391
|
|
|
286
392
|
session.messages = [
|