deel-local-cli 1.10.0 → 1.13.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 +2 -2
- package/src/acp/map.js +367 -362
- package/src/acp/serve.js +810 -769
- package/src/agent/budget.js +156 -153
- package/src/agent/commit.js +534 -511
- package/src/agent/compact.js +158 -31
- 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 +1728 -1516
- package/src/agent/memory.js +156 -152
- package/src/agent/mention.js +210 -210
- package/src/agent/recall.js +222 -209
- package/src/agent/session.js +1056 -948
- package/src/backend/adapter.js +1355 -798
- package/src/backend/cachemark.js +223 -0
- package/src/backend/detect.js +327 -288
- package/src/backend/http.js +460 -458
- package/src/backend/mcp.js +407 -395
- package/src/backend/price.js +260 -197
- package/src/backend/probe.js +487 -473
- package/src/backend/quota.js +274 -157
- package/src/backend/retry.js +30 -6
- package/src/backend/tokens.js +59 -0
- package/src/backend/toolfit.js +352 -325
- package/src/backend/wire.js +715 -0
- package/src/commands.js +3184 -2998
- package/src/config.js +283 -244
- package/src/i18n/en.js +545 -520
- package/src/i18n/ja.js +498 -473
- package/src/i18n/ko.js +592 -567
- package/src/i18n/zh.js +498 -473
- package/src/oneshot.js +625 -549
- package/src/pack/tar.js +154 -132
- package/src/pack/zip.js +235 -217
- package/src/plugins/manage.js +416 -336
- package/src/providers/bedrock.js +17 -0
- package/src/repl.js +2616 -2515
- package/src/safety/audit.js +148 -129
- package/src/safety/guard.js +641 -526
- package/src/safety/network.js +200 -157
- package/src/tools/fastgrep.js +229 -214
- package/src/tools/fsutil.js +291 -250
- package/src/tools/index.js +2560 -2523
- package/src/tools/jobs.js +876 -795
- package/src/tools/label.js +52 -0
- package/src/tools/spawn.js +213 -99
- package/src/tools/verify.js +358 -357
- package/src/tools/webfetch.js +418 -393
- package/src/ui/export.js +233 -217
- package/src/ui/pick.js +115 -115
- package/src/ui/status.js +617 -610
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,47 @@ 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
|
+
/*
|
|
144
|
+
* 이름과 인자를 **여기서** 뽑아 둔다.
|
|
145
|
+
*
|
|
146
|
+
* 아래 접는 자리가 이걸 안 쓰고 `이름표.get(m.tool_call_id)` 로 다시
|
|
147
|
+
* 뽑고 있었다. `tool_call_id` 는 OpenAI 꼴 짐작이라 Anthropic·Ollama
|
|
148
|
+
* 에서는 늘 undefined 였고, 그러면 이름이 「도구」로, 인자가 `{}` 로
|
|
149
|
+
* 떨어진다. 인자가 비면 경로도 못 뽑아서 파일 기억이 안 지워지고,
|
|
150
|
+
* 접힌 파일을 다시 읽을 때 「앞에서 읽은 그대로입니다」 만 돌아온다.
|
|
151
|
+
* 같은 것을 두 번 뽑을 까닭이 없다 — 한 번 뽑아서 같이 들고 간다.
|
|
152
|
+
*/
|
|
153
|
+
const 아는것 = 이름표.get(첫?.id) ?? null;
|
|
154
|
+
자리.push({
|
|
155
|
+
i, 글,
|
|
156
|
+
이름: 아는것?.name ?? 첫?.name ?? '',
|
|
157
|
+
args: 아는것?.args ?? {},
|
|
158
|
+
});
|
|
119
159
|
});
|
|
120
160
|
|
|
121
161
|
/*
|
|
@@ -137,6 +177,19 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN }
|
|
|
137
177
|
const 접을것 = 자리
|
|
138
178
|
.slice(0, Math.max(0, 자리.length - keep))
|
|
139
179
|
.filter((x) => x.글.length >= min && x.i !== 지킬할일);
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* 비울 수 있는 양이 문턱에 못 미치면 **이번에는 안 접는다.**
|
|
183
|
+
*
|
|
184
|
+
* 한 줄 아끼자고 앞머리를 깨면 그 걸음에 대화가 통째로 다시 나간다.
|
|
185
|
+
* 모아 두었다가 한 번에 접는 편이 눈에 띄게 싸다. 자리가 정말 모자라면
|
|
186
|
+
* 80% 에서 요약 압축이 받아 준다 — 안전망은 그쪽에 있다.
|
|
187
|
+
*/
|
|
188
|
+
const 예상이득 = 접을것.reduce((a, x) => a + estimateTokens(x.글), 0);
|
|
189
|
+
if (예상이득 < 이득문턱) {
|
|
190
|
+
return { 접은것: 0, 아낀토큰: 0, 접은것들: [], 미룸: true, 모인것: 예상이득 };
|
|
191
|
+
}
|
|
192
|
+
|
|
140
193
|
let 아낀토큰 = 0;
|
|
141
194
|
// 무엇을 버렸는지 이름으로 남긴다.
|
|
142
195
|
//
|
|
@@ -145,25 +198,64 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN }
|
|
|
145
198
|
// 사람 화면에는 여기서 돌려주는 목록으로 알려 준다.
|
|
146
199
|
const 접은것들 = [];
|
|
147
200
|
|
|
148
|
-
for (const { i,
|
|
201
|
+
for (const { i, 글, 이름, args } of 접을것) {
|
|
149
202
|
const m = ms[i];
|
|
150
|
-
const
|
|
151
|
-
const 곳 = 어디(
|
|
203
|
+
const 도구이름 = 이름 || '도구';
|
|
204
|
+
const 곳 = 어디(args);
|
|
152
205
|
const 줄수 = 글.split('\n').length;
|
|
153
206
|
const 전 = estimateTokens(글);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
207
|
+
const 접은글 = `${접힘표} ${도구이름}${곳 ? `(${곳})` : ''} — ${줄수}줄. `
|
|
208
|
+
+ '자리를 비우려고 내용을 접었습니다. 필요하면 다시 읽으세요.';
|
|
209
|
+
/*
|
|
210
|
+
* 글만 갈아 끼운다 — 메시지를 통째로 덮으면 짝이 깨진다.
|
|
211
|
+
*
|
|
212
|
+
* Anthropic 꼴 결과는 `tool_result` 블록 안에 `tool_use_id` 를 들고 있다.
|
|
213
|
+
* `{ ...m, content: 접은글 }` 로 덮으면 그 id 가 없어지고, 앞 차례의
|
|
214
|
+
* `tool_use` 가 짝을 잃어 다음 요청이 400 이다. 규격별로 어디를 바꿔야
|
|
215
|
+
* 하는지는 backend/adapter.js 가 안다 (결과바꾸기).
|
|
216
|
+
*/
|
|
217
|
+
ms[i] = 결과바꾸기(m, 접은글);
|
|
218
|
+
const 아낀것 = 전 - estimateTokens(접은글);
|
|
160
219
|
아낀토큰 += 아낀것;
|
|
161
|
-
|
|
220
|
+
/*
|
|
221
|
+
* 자른 표시용 이름(곳)과 **진짜 경로**를 따로 담는다.
|
|
222
|
+
*
|
|
223
|
+
* 곳 은 화면에 쓰려고 60자에서 자른 것이라, 이걸로 파일 기억을 지우면
|
|
224
|
+
* 긴 경로가 안 맞아서 조용히 안 지워진다 — 그러면 접힌 파일을 다시
|
|
225
|
+
* 읽을 때 「앞에서 읽은 그대로입니다」 만 돌아간다(agent/filemem.js).
|
|
226
|
+
*/
|
|
227
|
+
const 경로 = typeof args?.file_path === 'string'
|
|
228
|
+
? args.file_path
|
|
229
|
+
: (typeof args?.path === 'string' ? args.path : null);
|
|
230
|
+
접은것들.push({ 도구: 도구이름, 곳, 경로, 줄수, 토큰: Math.max(0, 아낀것) });
|
|
162
231
|
}
|
|
163
232
|
|
|
164
233
|
return { 접은것: 접을것.length, 아낀토큰: Math.max(0, 아낀토큰), 접은것들 };
|
|
165
234
|
}
|
|
166
235
|
|
|
236
|
+
/**
|
|
237
|
+
* 접힌 파일을 **파일 기억에서 지울 때 쓸 열쇠** (agent/filemem.js).
|
|
238
|
+
*
|
|
239
|
+
* 접은 자리에 남은 경로는 모델이 적어 보낸 글자 그대로다 — 대개 `src/a.js`
|
|
240
|
+
* 같은 상대 경로다. 그런데 파일 기억은 `ctx.scope.resolve()` 를 거친 **절대
|
|
241
|
+
* 경로**로 묶여 있다(tools/index.js 의 읽은것). 그대로 지우면 Map 이 안 맞아서
|
|
242
|
+
* 조용히 아무것도 안 지워지고, 그러면 접혀 사라진 파일을 다시 읽을 때
|
|
243
|
+
* 「앞에서 읽은 그대로입니다」 가 돌아간다. 모델은 대화 어디에도 없는 글을
|
|
244
|
+
* 가리키는 쪽지를 받고, 결국 또 읽는다 — 접기로 아낀 것을 되읽기로 도로 쓴다.
|
|
245
|
+
*
|
|
246
|
+
* 울타리 밖이라 못 풀면 적힌 그대로 돌려준다. 둘 다 지워 보는 것은 부르는
|
|
247
|
+
* 쪽 몫이다 — 지우는 일은 없는 열쇠에도 안전하다.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} 경로 접은 자리에 적혀 있던 경로 (모델이 준 그대로)
|
|
250
|
+
* @param {object|null} scope safety/guard.js 의 울타리
|
|
251
|
+
* @returns {string} 지울 때 쓸 열쇠
|
|
252
|
+
*/
|
|
253
|
+
export function 접힌파일열쇠(경로, scope = null) {
|
|
254
|
+
const 적힌것 = String(경로 ?? '');
|
|
255
|
+
if (!적힌것) return 적힌것;
|
|
256
|
+
try { return scope?.resolve?.(적힌것) ?? 적힌것; } catch { return 적힌것; }
|
|
257
|
+
}
|
|
258
|
+
|
|
167
259
|
const 요약지시 = `지금까지의 대화를 다음 형식으로 요약하세요. 이어서 일할 사람이 이것만 보고도 계속할 수 있어야 합니다.
|
|
168
260
|
추측하지 말고 실제로 오간 내용만 쓰세요. 한국어로, 각 항목 3줄 이내로 쓰세요.
|
|
169
261
|
|
|
@@ -210,14 +302,20 @@ export function split(messages, { keepHead = KEEP_HEAD, tailRatio = KEEP_TAIL_RA
|
|
|
210
302
|
function transcript(msgs) {
|
|
211
303
|
const out = [];
|
|
212
304
|
for (const m of msgs) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
305
|
+
/*
|
|
306
|
+
* 누가 한 말인지도 규격을 봐야 안다. Anthropic 은 도구 결과를 **사람
|
|
307
|
+
* 차례**에 싣기 때문에, role 만 보면 도구가 뱉은 것이 전부 「사용자」 가
|
|
308
|
+
* 된다. 그러면 아래 400자 자르기도 안 걸려서, 요약을 시키는 요청에 도구
|
|
309
|
+
* 출력이 통째로 실려 나간다 — 요약하려는 대화보다 요약 요청이 더 커진다.
|
|
310
|
+
* 그리고 그렇게 만든 요약이 대화를 **대신하게** 된다.
|
|
311
|
+
*/
|
|
312
|
+
const 결과인가 = 도구결과인가(m);
|
|
313
|
+
const who = 결과인가 ? '도구결과' : (m.role === 'user' ? '사용자' : m.role === 'assistant' ? '나' : '도구결과');
|
|
314
|
+
let body = 결과인가
|
|
315
|
+
? 결과들(m).map((x) => x.글).join('\n')
|
|
316
|
+
: (typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''));
|
|
317
|
+
if (결과인가) body = body.slice(0, 400);
|
|
318
|
+
const calls = 부른것들(m).map((t) => `${t.name}(${JSON.stringify(t.args ?? {}).slice(0, 160)})`);
|
|
221
319
|
if (calls.length) out.push(`${who}: [도구] ${calls.join(', ')}`);
|
|
222
320
|
if (body.trim()) out.push(`${who}: ${body.trim()}`);
|
|
223
321
|
}
|
|
@@ -235,6 +333,8 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
235
333
|
|
|
236
334
|
const text = transcript(parts.fold);
|
|
237
335
|
let summary = null;
|
|
336
|
+
// 왜 못 받았는지. 「못 받았다」 만으로는 사람이 할 수 있는 일이 없다.
|
|
337
|
+
let 못한까닭 = null;
|
|
238
338
|
// 서버가 잠깐 막아 다시 부른 횟수. 알림은 부르는 쪽(onBackoff)이 화면에 내고, 셈은 여기서 한다.
|
|
239
339
|
let 다시 = 0;
|
|
240
340
|
const 셈하기 = (n) => { session.usage.retries = (session.usage.retries ?? 0) + Math.max(0, n); };
|
|
@@ -254,7 +354,20 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
254
354
|
timeout: 60000,
|
|
255
355
|
});
|
|
256
356
|
셈하기(다시);
|
|
257
|
-
|
|
357
|
+
/*
|
|
358
|
+
* 받은 것을 그대로 믿지 않는다. 이 요약은 **대화를 대신하게 될 글**이라,
|
|
359
|
+
* 반쪽이거나 거절이면 대화 전체가 그 반쪽으로 바뀐다.
|
|
360
|
+
*
|
|
361
|
+
* · 상한에 걸려 잘린 요약 — 비어 있지 않으니 그냥 통과했다. 그러면
|
|
362
|
+
* 대화가 문장 중간에서 끊긴 한 토막으로 바뀐다.
|
|
363
|
+
* · 거절 — 이 규격은 거절 글을 `content` 에 담는다(adapter.js). 그러면
|
|
364
|
+
* 대화 전체가 「그건 도와드릴 수 없습니다」 한 줄이 된다.
|
|
365
|
+
*
|
|
366
|
+
* 둘 다 `ok: true` 로 보고됐다. 그냥 줄이는 편이 훨씬 낫다.
|
|
367
|
+
*/
|
|
368
|
+
if (r?.거절) 못한까닭 = '요약을 거절당했습니다';
|
|
369
|
+
else if (wasCut(r)) 못한까닭 = '요약이 상한에서 잘렸습니다';
|
|
370
|
+
else summary = (r.content ?? '').trim();
|
|
258
371
|
} catch (err) {
|
|
259
372
|
// 사용자가 끊은 것은 실패가 아니다. 대화를 건드리지 않고 그대로 둔다 —
|
|
260
373
|
// 여기서 물러서기(trim)까지 해 버리면, 끊었는데 대화가 줄어 있게 된다.
|
|
@@ -263,6 +376,17 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
263
376
|
return { ok: false, aborted: true, why: '중단했습니다', folded: 0, before, after: before };
|
|
264
377
|
}
|
|
265
378
|
셈하기(다시);
|
|
379
|
+
/*
|
|
380
|
+
* 서버가 한 말을 **버리지 않는다.**
|
|
381
|
+
*
|
|
382
|
+
* 여태 `err` 를 통째로 흘렸다. 열쇠가 만료돼 401 이든, 게이트웨이가
|
|
383
|
+
* 모르는 칸에 400 을 내든, 모델 이름이 틀렸든 화면에는 늘 「요약을 받지
|
|
384
|
+
* 못해 그냥 줄였습니다」 한 줄이었다. 그러면 사람은 압축이 왜 매번
|
|
385
|
+
* 물러서는지 알 길이 없고, 고칠 데는 대개 한 줄이면 되는 것이었다.
|
|
386
|
+
*/
|
|
387
|
+
못한까닭 = err?.serverMessage
|
|
388
|
+
? `요약을 못 받았습니다 — ${String(err.serverMessage).replace(/\s+/g, ' ').trim().slice(0, 120)}`
|
|
389
|
+
: `요약을 못 받았습니다 — ${String(err?.message ?? err).slice(0, 120)}`;
|
|
266
390
|
summary = null;
|
|
267
391
|
}
|
|
268
392
|
|
|
@@ -280,7 +404,10 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
280
404
|
*/
|
|
281
405
|
if (folded > 0) session.파일기억?.잊기();
|
|
282
406
|
const after = session.breakdown().used;
|
|
283
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
ok: folded > 0, folded, before, after, fallback: true,
|
|
409
|
+
why: 못한까닭 ? `${못한까닭}. 그냥 줄였습니다` : '요약을 받지 못해 그냥 줄였습니다',
|
|
410
|
+
};
|
|
284
411
|
}
|
|
285
412
|
|
|
286
413
|
session.messages = [
|