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/loop.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// 에이전트 루프. 모델 → 도구 → 결과 → 모델 을 답이 나올 때까지 돈다.
|
|
2
2
|
// 화면에 그릴 것은 이벤트로 흘려보낸다 — 화면 코드와 섞지 않는다.
|
|
3
3
|
import { chat, chatStream, assistantMessage, toolMessage } from '../backend/adapter.js';
|
|
4
|
+
import { 그림메시지 } from '../backend/vision.js';
|
|
5
|
+
import { 어떻게할까 } from '../safety/policy.js';
|
|
4
6
|
import { toolSchemas, runTool, TOOLS, 파일현황 } from '../tools/index.js';
|
|
5
7
|
import { isMutating } from '../safety/guard.js';
|
|
6
8
|
import { effortFor, tokensFor, fullCap, wasCut, shiftLevel } from './effort.js';
|
|
7
9
|
import { 살린쓰기 } from './salvage.js';
|
|
8
10
|
import { 배울것, 길이문제인가 } from '../backend/learn.js';
|
|
9
|
-
import { compact, shouldCompact, shouldFold, foldToolResults } from './compact.js';
|
|
11
|
+
import { compact, shouldCompact, shouldFold, foldToolResults, foldImages } from './compact.js';
|
|
10
12
|
import { 걸음수, 하위걸음수, 요약길이 } from './budget.js';
|
|
11
13
|
import { Session } from './session.js';
|
|
12
14
|
import { 최대깊이, 하위모드, 하위요약 } from '../tools/task.js';
|
|
@@ -16,6 +18,35 @@ import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴도구 } from '
|
|
|
16
18
|
import { get as workMode } from './modes.js';
|
|
17
19
|
import { 지시말 } from '../i18n/index.js';
|
|
18
20
|
|
|
21
|
+
/*
|
|
22
|
+
* 콜백으로만 소식을 주는 부름을, 제너레이터가 중간에 내보낼 수 있는 모양으로 바꾼다.
|
|
23
|
+
*
|
|
24
|
+
* chat() 이나 compact() 는 await 하나로 끝나는 부름이라, 그 안에서 "서버가 막았다,
|
|
25
|
+
* 2초 기다린다" 같은 소식이 생겨도 화면으로 나올 길이 없었다. 소식을 우편함에
|
|
26
|
+
* 넣어 두고 받는 쪽이 하나씩 꺼내 yield 한다. 부름이 끝나면 소식()도 끝난다.
|
|
27
|
+
*
|
|
28
|
+
* const 편지 = 우편함((알려줘) => chat(conn, { ..., onBackoff: 알려줘 }));
|
|
29
|
+
* for await (const 소식 of 편지.소식()) yield 소식;
|
|
30
|
+
* const 답 = await 편지.부름;
|
|
31
|
+
*/
|
|
32
|
+
function 우편함(부르기) {
|
|
33
|
+
const 함 = [];
|
|
34
|
+
let 깨우기 = null;
|
|
35
|
+
let 끝 = false;
|
|
36
|
+
const 부름 = 부르기((소식) => { 함.push(소식); 깨우기?.(); });
|
|
37
|
+
부름.then(() => { 끝 = true; 깨우기?.(); }, () => { 끝 = true; 깨우기?.(); });
|
|
38
|
+
return {
|
|
39
|
+
부름,
|
|
40
|
+
async *소식() {
|
|
41
|
+
while (!끝 || 함.length) {
|
|
42
|
+
if (함.length) { yield 함.shift(); continue; }
|
|
43
|
+
await new Promise((r) => { 깨우기 = r; });
|
|
44
|
+
깨우기 = null;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
/**
|
|
20
51
|
* 아무 내용도 없는 답인가.
|
|
21
52
|
*
|
|
@@ -59,7 +90,7 @@ export function 묶기(calls) {
|
|
|
59
90
|
return out;
|
|
60
91
|
}
|
|
61
92
|
|
|
62
|
-
export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 } = {}) {
|
|
93
|
+
export async function* run(session, ctx, userText, { signal = null, 깊이 = 0, 그림들 = null, 끼어들기 = null } = {}) {
|
|
63
94
|
/*
|
|
64
95
|
* 되돌리기 턴은 **부모만** 연다.
|
|
65
96
|
*
|
|
@@ -72,7 +103,10 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
72
103
|
* 되돌린 일을 또 하려 든다.
|
|
73
104
|
*/
|
|
74
105
|
if (!깊이) session.턴시작(ctx.history.nextTurn());
|
|
75
|
-
|
|
106
|
+
// @ 로 그림을 지목했으면 그 말과 함께 실어 보낸다 (backend/vision.js).
|
|
107
|
+
session.push(그림들?.length
|
|
108
|
+
? 그림메시지(session.conn?.kind, { 글: userText, 그림들 })
|
|
109
|
+
: { role: 'user', content: userText });
|
|
76
110
|
ctx.audit.turn(깊이 ? `[하위작업 ${깊이}겹] ${userText}` : userText);
|
|
77
111
|
|
|
78
112
|
/*
|
|
@@ -222,6 +256,9 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
222
256
|
// 창이 좁으면 도구 설명을 줄여 싣는다 (budget.js 의 설명길이).
|
|
223
257
|
// 도구를 빼는 게 아니라 설명만 줄이므로 할 수 있는 일은 안 달라진다.
|
|
224
258
|
ctx: conn.ctx ?? null,
|
|
259
|
+
// 그림을 볼 수 있는 모델일 때만 Read 설명에 그림 이야기를 넣는다.
|
|
260
|
+
// 못 보는데 넣어 두면 모델이 화면 사진을 열려 들고, 그때마다 한 걸음이 헛간다.
|
|
261
|
+
vision: conn.vision === true,
|
|
225
262
|
});
|
|
226
263
|
|
|
227
264
|
/*
|
|
@@ -311,6 +348,36 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
311
348
|
|
|
312
349
|
while (steps < maxSteps) {
|
|
313
350
|
steps++;
|
|
351
|
+
|
|
352
|
+
/*
|
|
353
|
+
* ── 도중에 한 말을 여기서 받는다 (끼어들기) ──────────────────────────
|
|
354
|
+
*
|
|
355
|
+
* 일하는 동안 친 말은 여태 **턴이 끝날 때까지** 기다렸다. 로컬 모델은
|
|
356
|
+
* 한 턴이 몇 분씩 가는데, 그 사이에 "아, 그건 말고 저거" 라고 쳐도
|
|
357
|
+
* 모델은 하던 길을 끝까지 간다. 사람이 할 수 있는 것은 Ctrl+C 로
|
|
358
|
+
* 지금까지 한 것을 통째로 버리는 것뿐이었다.
|
|
359
|
+
*
|
|
360
|
+
* 그래서 걸음마다 **모델을 부르기 직전에** 받아 넣는다. 다음 부름에
|
|
361
|
+
* 그 말이 같이 실리니, 하던 일을 버리지 않고 방향만 튼다.
|
|
362
|
+
*
|
|
363
|
+
* 도구가 도는 중간에는 안 넣는다 — 도구 호출과 그 답 사이에 사람 말이
|
|
364
|
+
* 끼면 대화 모양이 깨져서 게이트웨이가 400 을 준다. 걸음의 머리는
|
|
365
|
+
* 언제나 도구 답이 다 붙은 뒤다.
|
|
366
|
+
*/
|
|
367
|
+
/*
|
|
368
|
+
* 첫 걸음에는 안 받는다.
|
|
369
|
+
*
|
|
370
|
+
* 첫 부름 전에 큐에 남아 있는 것은 **턴이 시작되기 전에** 친 말이다.
|
|
371
|
+
* 그건 원래 다음 턴이 될 것이었는데, 여기서 끌어다 넣으면 사람이 따로
|
|
372
|
+
* 시킨 두 가지가 한 턴으로 뭉친다. 끼어들기는 「가던 길을 트는 것」이지
|
|
373
|
+
* 「줄 서 있는 것을 당겨오는 것」이 아니다.
|
|
374
|
+
*/
|
|
375
|
+
const 끼어든말 = (깊이 || steps <= 1) ? null : 끼어들기?.();
|
|
376
|
+
if (끼어든말) {
|
|
377
|
+
session.push({ role: 'user', content: 끼어든말 });
|
|
378
|
+
ctx.audit.turn(`[도중에 끼어든 말] ${끼어든말}`);
|
|
379
|
+
yield { type: 'steer', text: 끼어든말 };
|
|
380
|
+
}
|
|
314
381
|
/*
|
|
315
382
|
* 이 모델이 실제로 어떻게 하고 있는지 센다 (agent/grade.js).
|
|
316
383
|
*
|
|
@@ -351,18 +418,40 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
351
418
|
// 흘러온 글을 우리도 모아 둔다. 중간에 끊기면 이것만이 남는 전부다 —
|
|
352
419
|
// chatStream 은 끝까지 가야 message 를 주므로, 끊긴 순간에는 msg 가 비어 있다.
|
|
353
420
|
let 흘린것 = '';
|
|
421
|
+
/*
|
|
422
|
+
* 서버가 잠깐 막아 기다렸다 다시 부른 횟수는 /cost 와 deel run --json 이 본다.
|
|
423
|
+
* 다만 **정말 다시 불렀을 때** 센다 — 알림을 낸 뒤 기다리다 Ctrl+C 로 끊기면
|
|
424
|
+
* 다시 부른 것이 아니다. 그래서 셈은 미뤄 두고, 다음 소식이 오면 그때 더한다.
|
|
425
|
+
*/
|
|
426
|
+
let 미룬셈 = 0;
|
|
427
|
+
const 셈하기 = () => { session.usage.retries = (session.usage.retries ?? 0) + 미룬셈; 미룬셈 = 0; };
|
|
428
|
+
const 끝셈 = (err) => { if (err?.name === 'Aborted') 미룬셈 = 0; else 셈하기(); };
|
|
354
429
|
const askModel = async function* (maxTokens, think, { 한번에 = false } = {}) {
|
|
355
430
|
if (conn.streaming && !한번에) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (ev.type === '
|
|
360
|
-
|
|
431
|
+
try {
|
|
432
|
+
for await (const ev of chatStream(conn, ask(maxTokens, think))) {
|
|
433
|
+
셈하기(); // 무슨 소식이든 왔다는 것은 앞의 다시 부름이 실제로 있었다는 뜻
|
|
434
|
+
if (ev.type === 'done') msg = ev.message;
|
|
435
|
+
else {
|
|
436
|
+
if (ev.type === 'content') 흘린것 += ev.text ?? '';
|
|
437
|
+
if (ev.type === 'backoff') 미룬셈 = 1;
|
|
438
|
+
yield ev;
|
|
439
|
+
}
|
|
361
440
|
}
|
|
362
|
-
}
|
|
441
|
+
} catch (err) { 끝셈(err); throw err; }
|
|
442
|
+
셈하기();
|
|
363
443
|
} else {
|
|
364
444
|
yield { type: 'waiting' };
|
|
365
|
-
|
|
445
|
+
/*
|
|
446
|
+
* 한 번에 받는 길은 await 하나라 알림이 흘러나오지 않는다. 그래서 부름을
|
|
447
|
+
* 띄워 두고, 알림이 오면 먼저 내보내고, 답이 오면 그때 끝낸다. 안 그러면
|
|
448
|
+
* 서버가 60초 기다리라 한 동안 화면에는 '생각 중' 만 떠 있다 — 사람은
|
|
449
|
+
* 모델이 느린 줄 알지만 실은 429 였다.
|
|
450
|
+
*/
|
|
451
|
+
const 편지 = 우편함((onBackoff) => chat(conn, { ...ask(maxTokens, think), onBackoff }));
|
|
452
|
+
for await (const 알림 of 편지.소식()) { 셈하기(); 미룬셈 = 1; yield 알림; }
|
|
453
|
+
try { msg = await 편지.부름; } catch (err) { 끝셈(err); throw err; }
|
|
454
|
+
셈하기();
|
|
366
455
|
if (msg.thinking) yield { type: 'thinking', text: msg.thinking };
|
|
367
456
|
if (msg.content) yield { type: 'content', text: msg.content };
|
|
368
457
|
}
|
|
@@ -524,6 +613,15 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
524
613
|
session.push(toolMessage(conn.kind, { callId: call.id, name: call.name, content: note }));
|
|
525
614
|
};
|
|
526
615
|
|
|
616
|
+
/*
|
|
617
|
+
* 이번 턴에 Read 로 연 그림들.
|
|
618
|
+
*
|
|
619
|
+
* 도구 결과가 다 들어간 **뒤에** 한꺼번에 붙인다. 하나 읽을 때마다 바로
|
|
620
|
+
* 붙이면 도구 결과 사이에 사람 말이 끼어드는데, 그러면 뒤에 오는 도구
|
|
621
|
+
* 결과들이 짝을 잃는다 — 게이트웨이가 통째로 400 을 준다.
|
|
622
|
+
*/
|
|
623
|
+
const 붙일그림 = [];
|
|
624
|
+
|
|
527
625
|
for (const 덩어리 of 묶기(msg.toolCalls)) {
|
|
528
626
|
// 돌리는 중에 끊었다면, 남은 것은 실행하지 않고 결과 자리만 채운다.
|
|
529
627
|
// 자리를 비우면 짝이 깨져 다음에 이어할 수 없다.
|
|
@@ -637,8 +735,33 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
637
735
|
attempted.add(key);
|
|
638
736
|
}
|
|
639
737
|
|
|
738
|
+
/*
|
|
739
|
+
* 적어 둔 규칙이 모드보다 먼저다 (safety/policy.js).
|
|
740
|
+
*
|
|
741
|
+
* 금지 — 물어보지도 않는다. 물어보는 것은 막는 것이 아니다.
|
|
742
|
+
* `npm test` 에 스무 번 y 를 친 손은 스물한 번째도 친다.
|
|
743
|
+
* 허락 — 모드가 뭐든 안 묻는다.
|
|
744
|
+
* 없으면 예전 그대로 모드가 정한다.
|
|
745
|
+
*
|
|
746
|
+
* 막았으면 **어디에 적힌 규칙인지까지** 말한다. 그 말이 없으면 사람은
|
|
747
|
+
* 제 설정을 고칠 수도, 관리자에게 무엇을 풀어 달라고 할 수도 없다.
|
|
748
|
+
*/
|
|
749
|
+
const 판정 = 어떻게할까(ctx.규칙들, call.name, call.args);
|
|
750
|
+
if (판정.답 === 'deny') {
|
|
751
|
+
ctx.audit?.blocked?.('규칙으로 금지됨', `${판정.출처}: ${판정.규칙}`);
|
|
752
|
+
거절(call, `${판정.출처}에 적힌 규칙 ${판정.규칙} 으로 막혀 있습니다.`
|
|
753
|
+
+ ' 이 방법은 쓸 수 없습니다 — 다른 길을 찾거나, 왜 필요한지 사용자에게 말하세요.'
|
|
754
|
+
+ ' 같은 것을 다시 부르지 마세요.');
|
|
755
|
+
if (막힘셈(call, '규칙 금지')) 멈출까 = '규칙으로 막힌 것을 계속 다시 부르고 있습니다';
|
|
756
|
+
yield {
|
|
757
|
+
type: 'tool', name: call.name, args: call.args, showLabel: true,
|
|
758
|
+
result: { error: `막힘 — ${판정.출처}의 ${판정.규칙}` },
|
|
759
|
+
};
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
640
763
|
// 모드에 따라 물어본다. 기본(auto)은 안 묻고 되돌리기로 대응한다.
|
|
641
|
-
const needsOk = session.mode === 'strict'
|
|
764
|
+
const needsOk = 판정.답 === 'allow' ? false : session.mode === 'strict'
|
|
642
765
|
? ['Write', 'Edit', 'Bash'].includes(call.name)
|
|
643
766
|
: session.mode === 'confirm'
|
|
644
767
|
? (call.name === 'Bash' && isMutating(call.args?.command))
|
|
@@ -810,6 +933,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
810
933
|
session.usage.out += 자식.usage.out;
|
|
811
934
|
session.usage.calls += 자식.usage.calls;
|
|
812
935
|
session.usage.ms += 자식.usage.ms;
|
|
936
|
+
session.usage.retries = (session.usage.retries ?? 0) + (자식.usage.retries ?? 0);
|
|
813
937
|
|
|
814
938
|
const 글 = 하위요약({
|
|
815
939
|
목적, 모드: 자식모드, 끝, 모델: 모델알림?.말 ?? null,
|
|
@@ -950,6 +1074,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
950
1074
|
name: call.name,
|
|
951
1075
|
content: 실을것,
|
|
952
1076
|
}));
|
|
1077
|
+
if (result.그림) 붙일그림.push(result.그림);
|
|
953
1078
|
yield {
|
|
954
1079
|
type: 'tool',
|
|
955
1080
|
name: call.name,
|
|
@@ -962,6 +1087,17 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
962
1087
|
}
|
|
963
1088
|
}
|
|
964
1089
|
|
|
1090
|
+
// 연 그림들을 사람 말 자리로 붙인다 (backend/vision.js).
|
|
1091
|
+
if (붙일그림.length) {
|
|
1092
|
+
const 이름들 = 붙일그림.map((g) => g.show).join(' · ');
|
|
1093
|
+
session.push(그림메시지(conn.kind, {
|
|
1094
|
+
글: 붙일그림.length === 1
|
|
1095
|
+
? `방금 연 그림입니다: ${이름들}`
|
|
1096
|
+
: `방금 연 그림 ${붙일그림.length}장입니다: ${이름들}`,
|
|
1097
|
+
그림들: 붙일그림,
|
|
1098
|
+
}));
|
|
1099
|
+
}
|
|
1100
|
+
|
|
965
1101
|
// 도구를 돌리다 끕어졌어도 모델이 한 말은 이미 대화에 들어가 있다. 그건 남는다.
|
|
966
1102
|
if (signal?.aborted) { yield { type: 'aborted', steps, kept: true }; return; }
|
|
967
1103
|
|
|
@@ -983,12 +1119,18 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
983
1119
|
if (shouldFold(session)) {
|
|
984
1120
|
const f = foldToolResults(session);
|
|
985
1121
|
if (f.접은것) yield { type: 'folded', ...f };
|
|
1122
|
+
// 그림은 사람 말 자리에 실려서 위 접기가 못 건드린다. 따로 뺀다.
|
|
1123
|
+
const g = foldImages(session);
|
|
1124
|
+
if (g.뺀것) yield { type: 'images_folded', ...g };
|
|
986
1125
|
}
|
|
987
1126
|
|
|
988
1127
|
// 컨텍스트가 차오르면 오래된 대화를 '요약해서' 접는다. 그냥 자르면 하던 일을 잊는다.
|
|
989
1128
|
if (shouldCompact(session)) {
|
|
990
1129
|
yield { type: 'compacting' };
|
|
991
|
-
|
|
1130
|
+
// 요약을 부르다 서버가 막으면 그 알림도 화면으로 — '접는 중' 뒤에 60초를 숨기지 않는다.
|
|
1131
|
+
const 편지 = 우편함((onBackoff) => compact(session, { auto: true, signal, onBackoff }));
|
|
1132
|
+
for await (const 알림 of 편지.소식()) yield 알림;
|
|
1133
|
+
const r = await 편지.부름;
|
|
992
1134
|
if (r.aborted) { yield { type: 'aborted', steps, kept: true }; return; }
|
|
993
1135
|
if (r.ok) yield { type: 'compacted', ...r };
|
|
994
1136
|
else yield { type: 'compact_failed', why: r.why };
|
package/src/agent/mention.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// 그래서 규칙을 하나로 뒀다: **실제로 있는 경로일 때만 붙인다.**
|
|
15
15
|
// 없으면 아무 말 없이 글자 그대로 둔다. 지목이 아니었을 테니 조용한 편이 맞다.
|
|
16
16
|
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
17
|
-
import { readTextFull } from '../tools/fsutil.js';
|
|
17
|
+
import { readTextFull, 내부살림 } from '../tools/fsutil.js';
|
|
18
|
+
import { 계보규칙읽기, 무시하나 } from '../tools/ignore.js';
|
|
19
|
+
import { 그림인가, 그림읽기, 크기말 } from '../backend/vision.js';
|
|
18
20
|
import { estimateTokens } from './session.js';
|
|
19
21
|
|
|
20
22
|
// 붙일 수 있는 최대 토큰. 부르는 쪽에서 컨텍스트에 맞춰 넘겨준다.
|
|
@@ -74,13 +76,19 @@ function 찾아보기(m, scope) {
|
|
|
74
76
|
return { why: 'missing', path: m.path };
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function 폴더내용(abs, show) {
|
|
79
|
+
function 폴더내용(abs, show, root = null) {
|
|
78
80
|
let 목록 = [];
|
|
79
81
|
try { 목록 = readdirSync(abs, { withFileTypes: true }); }
|
|
80
82
|
catch (err) { return `(폴더를 못 읽었습니다: ${err.message})`; }
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
83
|
+
// git 이 안 보는 것은 여기서도 안 늘어놓는다 — Glob · Grep 과 같은 규칙 (tools/ignore.js). 뺀 수는 적는다.
|
|
84
|
+
const rel = show === '.' ? '' : String(show).replace(/\\/g, '/');
|
|
85
|
+
const 규칙 = root ? 계보규칙읽기(root, rel) : [];
|
|
86
|
+
const 보임 = 규칙.length ? 목록.filter((e) => !무시하나(rel ? `${rel}/${e.name}` : e.name, e.isDirectory(), 규칙)) : 목록;
|
|
87
|
+
const 뺀 = 목록.length - 보임.length;
|
|
88
|
+
const 줄들 = 보임.slice(0, 폴더최대).map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
89
|
+
const 더 = 보임.length > 폴더최대 ? `\n… 전체 ${보임.length}개 중 ${폴더최대}개까지` : '';
|
|
90
|
+
const 건너뜀 = 뺀 ? `\n(.gitignore 로 ${뺀}개 건너뜀 — 경로를 직접 주면 Read 된다)` : '';
|
|
91
|
+
return `${show} 는 폴더입니다. 안에 든 것:\n${줄들.join('\n')}${더}${건너뜀}`;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
/**
|
|
@@ -96,9 +104,9 @@ function 폴더내용(abs, show) {
|
|
|
96
104
|
* 'Read 부터 하라' 며 되돌려 보내지 않는다. 잘린 파일은 안 적는다.
|
|
97
105
|
* 안 본 데를 본 것으로 치면 그 자리를 그냥 고쳐 버린다.
|
|
98
106
|
*/
|
|
99
|
-
export function expand(text, { scope = null, budget = 기본예산, seen = null, onRead = null } = {}) {
|
|
107
|
+
export function expand(text, { scope = null, budget = 기본예산, seen = null, onRead = null, 눈있나 = false } = {}) {
|
|
100
108
|
const 원문 = String(text ?? '');
|
|
101
|
-
const 빈답 = { text: 원문, attached: [], missing: [], blocked: [] };
|
|
109
|
+
const 빈답 = { text: 원문, attached: [], missing: [], blocked: [], 그림들: [] };
|
|
102
110
|
if (!원문.includes('@') || !scope?.resolve) return 빈답;
|
|
103
111
|
|
|
104
112
|
const 지목들 = findMentions(원문);
|
|
@@ -107,6 +115,7 @@ export function expand(text, { scope = null, budget = 기본예산, seen = null,
|
|
|
107
115
|
const attached = [];
|
|
108
116
|
const missing = [];
|
|
109
117
|
const blocked = [];
|
|
118
|
+
const 그림들 = [];
|
|
110
119
|
const 붙일것 = [];
|
|
111
120
|
const 본것 = new Set();
|
|
112
121
|
let 남은예산 = Math.max(200, Number(budget) || 기본예산);
|
|
@@ -115,6 +124,18 @@ export function expand(text, { scope = null, budget = 기본예산, seen = null,
|
|
|
115
124
|
const 자리 = 찾아보기(m, scope);
|
|
116
125
|
if (자리.why === 'blocked') { blocked.push({ path: 자리.path, why: 자리.note }); continue; }
|
|
117
126
|
if (자리.why === 'missing') { missing.push(자리.path); continue; }
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* 살림 파일은 @ 로도 못 붙인다.
|
|
130
|
+
*
|
|
131
|
+
* Read 도구는 .deel/config.json 을 막고 있었는데 **여기는 안 막고 있었다.**
|
|
132
|
+
* 그래서 `@.deel/config.json` 한 줄이면 게이트웨이 열쇠가 대화에 실려
|
|
133
|
+
* 그대로 바깥으로 나갔다. 도구는 막고 @ 는 안 막으면 막은 것이 아니다.
|
|
134
|
+
* (검사: test/mention-secret.test.js)
|
|
135
|
+
*/
|
|
136
|
+
const 살림 = 내부살림(자리.abs);
|
|
137
|
+
if (살림) { blocked.push({ path: 자리.path, why: 살림 }); continue; }
|
|
138
|
+
|
|
118
139
|
if (본것.has(자리.abs)) continue; // 같은 파일을 두 번 적지 않는다
|
|
119
140
|
본것.add(자리.abs);
|
|
120
141
|
|
|
@@ -124,7 +145,32 @@ export function expand(text, { scope = null, budget = 기본예산, seen = null,
|
|
|
124
145
|
|
|
125
146
|
try {
|
|
126
147
|
if (statSync(자리.abs).isDirectory()) {
|
|
127
|
-
몸통 = 폴더내용(자리.abs, show);
|
|
148
|
+
몸통 = 폴더내용(자리.abs, show, scope.root ?? null);
|
|
149
|
+
} else if (그림인가(자리.abs)) {
|
|
150
|
+
/*
|
|
151
|
+
* 그림은 글로 안 붙인다. 붙이면 깨진 글자 수천 자가 대화에 실린다.
|
|
152
|
+
*
|
|
153
|
+
* 볼 수 있는 모델이면 그림 자체를 이 말과 함께 싣고(repl.js → loop.js),
|
|
154
|
+
* 못 보는 모델이면 **바이트를 아예 안 싣는다.** 안 보이는 모델에게
|
|
155
|
+
* 보내 봐야 400 이 오거나, 더 나쁘게는 서버가 조용히 무시하고 답을
|
|
156
|
+
* 지어낸다. 그러면 사람은 모델이 화면을 봤다고 믿는다.
|
|
157
|
+
*/
|
|
158
|
+
const 것 = 눈있나 ? 그림읽기(자리.abs) : { ok: false, 왜: null, bytes: 0 };
|
|
159
|
+
if (것.ok) {
|
|
160
|
+
그림들.push({ b64: 것.b64, mime: 것.mime, bytes: 것.bytes, show });
|
|
161
|
+
몸통 = `(그림 · ${크기말(것.bytes)} — 이 말과 함께 붙였습니다)`;
|
|
162
|
+
attached.push({ path: 자리.abs, show, full: true, 그림: true, bytes: 것.bytes });
|
|
163
|
+
붙일것.push(`
|
|
164
|
+
|
|
165
|
+
--- ${show} ---
|
|
166
|
+
${몸통}
|
|
167
|
+
--- ${show} 끝 ---`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
몸통 = 눈있나
|
|
171
|
+
? `(못 붙였습니다: ${것.왜})`
|
|
172
|
+
: '(그림입니다. 지금 붙어 있는 모델은 그림을 못 봅니다 — 안 붙였습니다.)';
|
|
173
|
+
통째로 = false;
|
|
128
174
|
} else {
|
|
129
175
|
// 사내 파일은 CP949 가 흔하다. 그냥 읽으면 통째로 깨진다.
|
|
130
176
|
const 읽음 = readTextFull(자리.abs);
|
|
@@ -156,9 +202,9 @@ export function expand(text, { scope = null, budget = 기본예산, seen = null,
|
|
|
156
202
|
if (남은예산 <= 0) break;
|
|
157
203
|
}
|
|
158
204
|
|
|
159
|
-
if (!붙일것.length) return { text: 원문, attached, missing, blocked };
|
|
205
|
+
if (!붙일것.length) return { text: 원문, attached, missing, blocked, 그림들 };
|
|
160
206
|
const 머리 = attached.length === 1
|
|
161
207
|
? '아래는 사용자가 @ 로 지목한 파일입니다. 이미 읽은 것으로 치고 답하세요.'
|
|
162
208
|
: `아래는 사용자가 @ 로 지목한 파일 ${attached.length}개입니다. 이미 읽은 것으로 치고 답하세요.`;
|
|
163
|
-
return { text: `${원문}\n\n${머리}${붙일것.join('')}`, attached, missing, blocked };
|
|
209
|
+
return { text: `${원문}\n\n${머리}${붙일것.join('')}`, attached, missing, blocked, 그림들 };
|
|
164
210
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// `/review` — 이번에 바꾼 것을 모델에게 한 번 더 보게 한다.
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 필요한가 ────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 쓴 사람은 제가 쓴 것을 못 본다. 방금 그 코드를 왜 그렇게 썼는지 머릿속에
|
|
6
|
+
// 남아 있어서, 읽을 때 그 기억이 빈자리를 메워 준다. 모델도 똑같다 — 같은
|
|
7
|
+
// 대화 안에서 "다시 봐" 라고 하면 제가 한 말을 근거로 제 코드를 변호한다.
|
|
8
|
+
//
|
|
9
|
+
// 그래서 **새 창에서** 본다. 지금 대화는 한 줄도 안 보낸다. 보내는 것은
|
|
10
|
+
// 바뀐 코드와 검사표뿐이다. 무엇을 하려던 것인지 모르는 눈으로 봐야
|
|
11
|
+
// "이건 왜 이렇게 돼 있지" 가 나온다.
|
|
12
|
+
//
|
|
13
|
+
// ── 무엇을 안 하나 ─────────────────────────────────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// **고치지 않는다.** 도구를 아예 안 준다. 찾은 것을 말로만 돌려준다.
|
|
16
|
+
// 리뷰가 제 손으로 고치기 시작하면 사람이 무엇을 승인한 것인지 흐려지고,
|
|
17
|
+
// 찾은 것 중 무엇이 진짜인지 가릴 기회가 없어진다. 고칠지 말지는 사람이
|
|
18
|
+
// 정한다 — 그게 리뷰의 전부다.
|
|
19
|
+
import { 깃, 저장소뿌리, 이번에바꾼것 } from './commit.js';
|
|
20
|
+
import { chat } from '../backend/adapter.js';
|
|
21
|
+
|
|
22
|
+
/** 모델에게 보여 줄 diff 길이 상한(글자). 넘으면 자르고 잘랐다고 적는다. */
|
|
23
|
+
const DIFF상한 = 60000;
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* 검사표.
|
|
27
|
+
*
|
|
28
|
+
* 고정해 둔다. 매번 다른 것을 보면 지난번에 통과한 것이 이번에 걸리고,
|
|
29
|
+
* 사람은 그 흔들림을 보고 리뷰를 안 믿게 된다. 그리고 여기 적힌 순서가
|
|
30
|
+
* 곧 중요도다 — 위에서부터 본다.
|
|
31
|
+
*/
|
|
32
|
+
export const 검사표 = [
|
|
33
|
+
['맞나', '이 코드가 하려는 일을 실제로 하는가. 경계값·빈 값·오류 갈래에서 어떻게 되나'],
|
|
34
|
+
['숨은 실패', '오류를 삼키는 자리가 있나. 실패했는데 성공한 것처럼 보이는 길이 있나'],
|
|
35
|
+
['정직한가', '화면에 나가는 말과 실제로 일어난 일이 같은가. 짐작한 값을 사실처럼 적지 않나'],
|
|
36
|
+
['안전', '입력을 그대로 명령·경로·주소로 쓰는 자리가 있나. 비밀이 로그·오류·파일로 새나'],
|
|
37
|
+
['되돌릴 수 있나', '고장 났을 때 되돌릴 방법이 있나. 지우거나 덮어쓰기 전에 확인하나'],
|
|
38
|
+
['검사', '이 변경이 깨졌을 때 빨개지는 검사가 있나. 없으면 어디에 무엇을 넣어야 하나'],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const 지시 = `너는 방금 바뀐 코드를 보는 리뷰어다. 이 코드를 쓴 사람이 아니다.
|
|
42
|
+
|
|
43
|
+
**고치지 마라.** 너에게는 도구가 없다. 찾은 것만 말한다.
|
|
44
|
+
|
|
45
|
+
무엇을 보나 — 위에서부터 중요한 순서다:
|
|
46
|
+
${검사표.map(([이름, 뜻], i) => `${i + 1}. ${이름} — ${뜻}`).join('\n')}
|
|
47
|
+
|
|
48
|
+
어떻게 쓰나:
|
|
49
|
+
- 찾은 것마다 **파일:줄** 로 자리를 짚는다. 자리를 못 짚겠으면 그 지적은 쓰지 마라 —
|
|
50
|
+
사람이 찾아갈 수 없는 지적은 아무 일도 못 하게 한다.
|
|
51
|
+
- **무슨 입력에서 무엇이 잘못되는지**를 쓴다. "예외 처리가 부족합니다" 는 지적이 아니다.
|
|
52
|
+
"파일이 없으면 undefined 가 넘어가 두 줄 뒤에서 터진다" 가 지적이다.
|
|
53
|
+
- diff 에 **보이는 것만** 쓴다. 안 보이는 파일의 내용을 짐작해서 쓰지 마라.
|
|
54
|
+
- 심각한 순서로 놓는다. 사소한 취향은 맨 뒤에 한 줄로 묶거나 아예 쓰지 마라.
|
|
55
|
+
- **아무것도 못 찾았으면 못 찾았다고 한 줄로 답한다.** 채우려고 지어내지 마라.
|
|
56
|
+
지어낸 지적 하나가 진짜 지적 열 개를 못 믿게 만든다.
|
|
57
|
+
|
|
58
|
+
형식:
|
|
59
|
+
심각 / 보통 / 사소 중 하나로 시작하는 줄을 만들고, 그 아래 자리와 까닭을 쓴다.
|
|
60
|
+
|
|
61
|
+
심각 src/a.js:42
|
|
62
|
+
열쇠가 오류 문구에 그대로 실린다. 키에 줄바꿈이 섞이면 …
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 무엇을 볼지 고른다.
|
|
67
|
+
*
|
|
68
|
+
* 이번 대화가 바꾼 것이 있으면 그것만 본다 — 사람이 방금 시킨 일이 그것이라
|
|
69
|
+
* 지금 궁금한 것도 그것이다. 없으면 저장소가 들고 있는 변경(작업 트리)을 본다.
|
|
70
|
+
*
|
|
71
|
+
* @returns {{ok:boolean, 어디:string, diff:string, 통계:string, 파일들:string[], 왜?:string}}
|
|
72
|
+
*/
|
|
73
|
+
export function 볼것(session, ctx) {
|
|
74
|
+
const 여기 = ctx?.scope?.root ?? session?.root ?? process.cwd();
|
|
75
|
+
const 뿌리 = 저장소뿌리(여기);
|
|
76
|
+
if (!뿌리) return { ok: false, 왜: '여기는 git 저장소가 아닙니다 — 무엇이 바뀌었는지 알 방법이 없습니다.' };
|
|
77
|
+
|
|
78
|
+
// 이번 대화가 손댄 파일 (agent/commit.js 가 쓰는 것과 같은 셈법 — 살림 폴더와
|
|
79
|
+
// 저장소 밖은 빠지고, 폴더째 적힌 것은 안 딸려 온다).
|
|
80
|
+
const 안쪽 = [...이번에바꾼것(session, 뿌리)];
|
|
81
|
+
|
|
82
|
+
const 인자 = 안쪽.length
|
|
83
|
+
? ['diff', 'HEAD', '--', ...안쪽]
|
|
84
|
+
: ['diff', 'HEAD'];
|
|
85
|
+
const 몸통 = 깃(뿌리, 인자, {});
|
|
86
|
+
const 통계 = 깃(뿌리, [...인자.slice(0, 2), '--stat', ...인자.slice(2)], {});
|
|
87
|
+
const 이름 = 깃(뿌리, [...인자.slice(0, 2), '--name-only', ...인자.slice(2)], {});
|
|
88
|
+
const 파일들 = 이름.out.split('\n').map((x) => x.trim()).filter(Boolean);
|
|
89
|
+
|
|
90
|
+
if (!몸통.out.trim()) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
왜: 안쪽.length
|
|
94
|
+
? '이번 대화가 바꾼 파일이 저장소 기준으로는 그대로입니다 — 볼 것이 없습니다.'
|
|
95
|
+
: '바뀐 것이 없습니다.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
어디: 안쪽.length ? '이번 대화가 바꾼 것' : '저장소의 바뀐 것 전부',
|
|
101
|
+
diff: 몸통.out,
|
|
102
|
+
통계: 통계.out.trimEnd(),
|
|
103
|
+
파일들,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 리뷰어에게 보낼 한 덩이. 지금 대화는 한 줄도 안 들어간다. */
|
|
108
|
+
export function 보낼것({ diff, 통계, 파일들 }) {
|
|
109
|
+
const 자른것 = diff.length > DIFF상한
|
|
110
|
+
? `${diff.slice(0, DIFF상한)}\n… (diff 가 길어 여기서 잘랐습니다 — 잘린 뒤는 못 봤습니다)`
|
|
111
|
+
: diff;
|
|
112
|
+
return [
|
|
113
|
+
지시,
|
|
114
|
+
`바뀐 파일:\n${통계 || 파일들.map((f) => `- ${f}`).join('\n')}`,
|
|
115
|
+
`----- diff -----\n${자른것}`,
|
|
116
|
+
].join('\n\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* 답을 줄로 가른다.
|
|
121
|
+
*
|
|
122
|
+
* 모델이 형식을 안 지킬 때가 있다. 그때 통째로 버리면 사람은 아무것도 못
|
|
123
|
+
* 본다 — 그래서 못 가른 줄은 **그대로 보여 준다.** 우리가 못 읽은 것이지
|
|
124
|
+
* 모델이 아무 말도 안 한 것이 아니다.
|
|
125
|
+
*/
|
|
126
|
+
/*
|
|
127
|
+
* 낱말 경계(`\b`)를 한글 뒤에 못 쓴다.
|
|
128
|
+
*
|
|
129
|
+
* 자바스크립트의 낱말 경계는 ASCII 기준이라 '심각' 뒤에는 경계가 안 생긴다.
|
|
130
|
+
* `/^심각\b/` 는 "심각 a.js:1" 에 **안 걸린다.** 그래서 한글은 뒤에 올 수
|
|
131
|
+
* 있는 글자로 끊고, 영어에만 `\b` 를 쓴다. 조용히 안 걸리는 규칙은 없느니만
|
|
132
|
+
* 못하다 — 리뷰가 아무것도 못 찾은 것처럼 보인다.
|
|
133
|
+
*/
|
|
134
|
+
const 급표 = [
|
|
135
|
+
[/^\s*심각(?=[\s:.·—-]|$)/, '심각'],
|
|
136
|
+
[/^\s*(critical|high)\b/i, '심각'],
|
|
137
|
+
[/^\s*보통(?=[\s:.·—-]|$)/, '보통'],
|
|
138
|
+
[/^\s*(medium|moderate)\b/i, '보통'],
|
|
139
|
+
[/^\s*사소(?=[\s:.·—-]|$)/, '사소'],
|
|
140
|
+
[/^\s*(low|minor|nit)\b/i, '사소'],
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export function 찾은것가르기(글) {
|
|
144
|
+
const 줄들 = String(글 ?? '').split('\n');
|
|
145
|
+
const 것들 = [];
|
|
146
|
+
let 지금 = null;
|
|
147
|
+
for (const 줄 of 줄들) {
|
|
148
|
+
const 급 = 급표.find(([re]) => re.test(줄));
|
|
149
|
+
if (급) {
|
|
150
|
+
if (지금) 것들.push(지금);
|
|
151
|
+
// 자리(파일:줄)를 같은 줄에서 찾아 둔다. 없으면 아래 줄에서 찾는다.
|
|
152
|
+
지금 = { 급: 급[1], 머리: 줄.trim(), 몸: [], 자리: 자리찾기(줄) };
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (지금) {
|
|
156
|
+
지금.몸.push(줄);
|
|
157
|
+
if (!지금.자리) 지금.자리 = 자리찾기(줄);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (지금) 것들.push(지금);
|
|
161
|
+
return 것들;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 리뷰어를 부른다.
|
|
166
|
+
*
|
|
167
|
+
* 지금 대화는 한 줄도 안 보낸다. 도구도 안 준다 — 새 창에서, 손 없이 본다.
|
|
168
|
+
* 실패하면 **지어내지 않는다.** 못 봤으면 못 봤다고 돌려준다.
|
|
169
|
+
*/
|
|
170
|
+
export async function 리뷰받기(session, 것, { signal = null, onBackoff = null } = {}) {
|
|
171
|
+
try {
|
|
172
|
+
const r = await chat(session.conn, {
|
|
173
|
+
messages: [{ role: 'user', content: 보낼것(것) }],
|
|
174
|
+
maxTokens: 2000,
|
|
175
|
+
think: session.conn.kind === 'ollama' ? false : 'low',
|
|
176
|
+
signal,
|
|
177
|
+
onBackoff,
|
|
178
|
+
timeout: 180000,
|
|
179
|
+
});
|
|
180
|
+
const 글 = (r?.content ?? '').trim();
|
|
181
|
+
if (!글) return { ok: false, 왜: '모델이 빈 답을 줬습니다 — 다시 시켜 보세요.' };
|
|
182
|
+
return { ok: true, 글, 찾은것: 찾은것가르기(글) };
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return { ok: false, 왜: err.message };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** `경로:줄` 을 찾는다. 못 찾으면 null — 지어내지 않는다. */
|
|
189
|
+
export function 자리찾기(줄) {
|
|
190
|
+
const m = /([\w./\\가-힣-]+\.[A-Za-z0-9]{1,8}):(\d+)/.exec(String(줄 ?? ''));
|
|
191
|
+
return m ? { 파일: m[1], 줄: Number(m[2]) } : null;
|
|
192
|
+
}
|
package/src/agent/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 대화 상태와 컨텍스트 셈. /context 가 보여주는 숫자가 여기서 나온다.
|
|
2
2
|
import { readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { 그림장수, 글만, 그림한장토큰 } from '../backend/vision.js';
|
|
4
5
|
import { get as workMode, 말 as 모드말, DEFAULT as WORK_DEFAULT } from './modes.js';
|
|
5
6
|
import { toolSchemas } from '../tools/index.js';
|
|
6
7
|
import { normalize as normLevel, DEFAULT as LEVEL_DEFAULT } from '../ui/level.js';
|
|
@@ -9,6 +10,7 @@ import { 지문 } from './project.js';
|
|
|
9
10
|
import { 프롬프트토막 as 기억토막 } from './memory.js';
|
|
10
11
|
import { 못박기 } from './pins.js';
|
|
11
12
|
import { 언어, 지시말 } from '../i18n/index.js';
|
|
13
|
+
import { 셸안내 } from '../tools/shell.js';
|
|
12
14
|
|
|
13
15
|
// 토큰 추정 — 정확한 토크나이저 없이 대략만 센다.
|
|
14
16
|
// 한글은 글자당 약 1토큰, 영문·코드는 약 4글자당 1토큰으로 본다.
|
|
@@ -252,7 +254,7 @@ export class Session {
|
|
|
252
254
|
this.plugins = [];
|
|
253
255
|
this.maxSkillsListed = 40; // 프롬프트에 올릴 최대 개수
|
|
254
256
|
this.maxSkillDesc = 140; // 설명 한 줄 최대 길이
|
|
255
|
-
this.usage = { in: 0, out: 0, calls: 0, ms: 0 };
|
|
257
|
+
this.usage = { in: 0, out: 0, calls: 0, ms: 0, retries: 0 };
|
|
256
258
|
/*
|
|
257
259
|
* 지금 붙은 모델이 얼마나 하는가 (agent/grade.js).
|
|
258
260
|
*
|
|
@@ -319,6 +321,10 @@ export class Session {
|
|
|
319
321
|
parts.push(영
|
|
320
322
|
? `\nWorking folder: ${this.root}\nYou can neither read nor write files outside this folder.`
|
|
321
323
|
: `\n작업 폴더: ${this.root}\n이 폴더 밖의 파일은 읽지도 쓰지도 못한다.`);
|
|
324
|
+
// 어느 셸에서 명령이 도는지 한 줄 (tools/shell.js). 모델이 ls 를 칠지 dir 를 칠지가
|
|
325
|
+
// 여기서 갈린다 — cmd 에서 유닉스 명령을 치면 한 번에 20~40초짜리 헛걸음이다.
|
|
326
|
+
// 세션 안에서는 안 변하는 줄이라 앞머리(캐시되는 자리)에 둔다.
|
|
327
|
+
parts.push(셸안내(영));
|
|
322
328
|
|
|
323
329
|
/*
|
|
324
330
|
* 모델 급에 맞춘 한 문단 (grade.js).
|
|
@@ -598,7 +604,18 @@ export class Session {
|
|
|
598
604
|
let history = 0;
|
|
599
605
|
let files = 0;
|
|
600
606
|
for (const m of this.messages) {
|
|
601
|
-
|
|
607
|
+
/*
|
|
608
|
+
* 그림은 글자 수로 세지 않는다.
|
|
609
|
+
*
|
|
610
|
+
* 그림은 base64 로 실려 있어서 글로 세면 4MB 짜리 한 장이 150만 토큰으로
|
|
611
|
+
* 잡힌다. 그러면 창이 다 찬 줄 알고 대화를 통째로 접는다 — 화면 사진 한 장
|
|
612
|
+
* 보여 준 값으로 하던 일을 잃는 셈이다. 한 장에 얼마인지는 서버가 정하는
|
|
613
|
+
* 것이라 우리는 모르므로, 정해 둔 값으로 세고 서버가 알려주는 실제 값으로
|
|
614
|
+
* 고쳐 나간다 (backend/vision.js 의 그림한장토큰 머리말).
|
|
615
|
+
*/
|
|
616
|
+
const 장수 = 그림장수(m);
|
|
617
|
+
const t = estimateTokens(장수 ? 글만(m) : (typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '')))
|
|
618
|
+
+ 장수 * 그림한장토큰
|
|
602
619
|
+ estimateTokens(JSON.stringify(m.tool_calls ?? ''));
|
|
603
620
|
if (m.role === 'tool') files += t; else history += t;
|
|
604
621
|
}
|
|
@@ -673,6 +690,7 @@ export class Session {
|
|
|
673
690
|
// 재게 되고, 그러면 /context 가 실제보다 크게 말한다 — 그 값으로
|
|
674
691
|
// effort.js 가 출력 상한을 잡으므로 답이 이유 없이 짧아진다.
|
|
675
692
|
ctx: this.conn?.ctx ?? null,
|
|
693
|
+
vision: this.conn?.vision === true,
|
|
676
694
|
});
|
|
677
695
|
n = estimateTokens(JSON.stringify(list));
|
|
678
696
|
} catch { n = 0; }
|
package/src/agent/threads.js
CHANGED