deel-local-cli 1.20.8 → 1.20.10
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 +4 -2
- package/README.md +4 -2
- package/bin/deel.js +27 -0
- package/package.json +1 -1
- package/src/agent/effort.js +25 -2
- package/src/agent/evidence.js +12 -2
- package/src/agent/loop.js +3 -1
- package/src/agent/models.js +22 -0
- package/src/agent/modes.js +67 -7
- package/src/agent/session.js +13 -2
- package/src/backend/ctxsize.js +17 -3
- package/src/commands.js +251 -31
- package/src/configexplain.js +15 -3
- package/src/i18n/en.js +2 -1
- package/src/i18n/ko.js +2 -1
- package/src/repl.js +379 -24
- package/src/tools/index.js +11 -1
- package/src/tools/pdf.js +168 -44
- package/src/ui/ansi.js +30 -8
- package/src/ui/export.js +5 -2
- package/src/ui/inputbox.js +105 -25
- package/src/ui/level.js +9 -1
- package/src/ui/office.js +16 -2
- package/src/ui/pastechip.js +3 -0
- package/src/ui/status.js +46 -5
package/src/repl.js
CHANGED
|
@@ -39,12 +39,12 @@ import { 못박기 } from './agent/pins.js';
|
|
|
39
39
|
import { 카드 } from './agent/card.js';
|
|
40
40
|
import { 배움 } from './agent/evolve.js';
|
|
41
41
|
import { 마크다운 } from './ui/md.js';
|
|
42
|
-
import { askHidden
|
|
42
|
+
import { askHidden } from './ui/prompt.js';
|
|
43
43
|
import { explain, shows as levelShows } from './ui/level.js';
|
|
44
44
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
45
45
|
import { 추천, 채울글 } from './ui/complete.js';
|
|
46
46
|
import { 접어쓰기 } from './ui/wrap.js';
|
|
47
|
-
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들,
|
|
47
|
+
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들, 남길글, 안쪽최대 as 붙임안쪽최대 } from './ui/pastechip.js';
|
|
48
48
|
import { 고른것풀기, 계획답풀기 } from './ui/pick.js';
|
|
49
49
|
import { 이력지킴이 } from './ui/histline.js';
|
|
50
50
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
@@ -80,7 +80,7 @@ const TOOL_GLYPH = {
|
|
|
80
80
|
function toolLabel(name, args) {
|
|
81
81
|
const a = args ?? {};
|
|
82
82
|
// 이름 차례는 tools/label.js 한 곳에서 온다 (다섯 벌이던 것을 모았다).
|
|
83
|
-
// 여기서 더 하는 일은 화면 사정뿐이다 —
|
|
83
|
+
// 여기서 더 하는 일은 화면 사정뿐이다 — 56자에서 자른다.
|
|
84
84
|
const first =
|
|
85
85
|
첫이름(a) ??
|
|
86
86
|
(a.command ? String(a.command).replace(/\s+/g, ' ').slice(0, 52) : null) ??
|
|
@@ -105,7 +105,11 @@ function toolLabel(name, args) {
|
|
|
105
105
|
(Array.isArray(a.paths) && a.paths.length ? 세말('count', a.paths.length) : null) ??
|
|
106
106
|
// 할 일 목록은 보여줄 경로가 없다. 빈 괄호를 띄우느니 개수를 적는다.
|
|
107
107
|
(Array.isArray(a.todos) ? 세말('hits', a.todos.length) : null) ?? '';
|
|
108
|
-
|
|
108
|
+
// 물려받은 이름으로 오면 `?? ` 가 안 걸린다 — TOOL_GLYPH['constructor'] 는
|
|
109
|
+
// 함수라서 참이고, 그대로 글로 바뀌어 `function Object() { [native code] }`
|
|
110
|
+
// 가 도구 줄에 찍힌다. 모델이 그런 이름으로 도구를 부르면 실제로 여기 온다
|
|
111
|
+
// (loop.js 가 모르는 도구도 showLabel 로 올린다).
|
|
112
|
+
const g = Object.hasOwn(TOOL_GLYPH, name) ? TOOL_GLYPH[name] : c.cyan('⏺');
|
|
109
113
|
const 안 = clip(String(first ?? ''), 56);
|
|
110
114
|
return `${g} ${c.bold(name)}${안 ? `${c.gray('(')}${c.gray(안)}${c.gray(')')}` : ''}`;
|
|
111
115
|
}
|
|
@@ -352,7 +356,20 @@ export async function chatLoop(opts = {}) {
|
|
|
352
356
|
say(` ${mark.warn} ${c.gray('이 연결은 열쇠를 받아 옵니다. 아래 명령을 띄웁니다:')}`);
|
|
353
357
|
say(` ${c.white(clip(설정.명령, 88))}`);
|
|
354
358
|
say(` ${c.gray('받은 열쇠는 이 판이 도는 동안 메모리에만 있고, 파일로 안 적습니다.')}`);
|
|
355
|
-
|
|
359
|
+
/*
|
|
360
|
+
* 이 자리는 repl 자신의 ask() 를 쓴다.
|
|
361
|
+
*
|
|
362
|
+
* 여기 있던 것은 ui/prompt.js 의 confirm 이었는데, 그 파일이 스스로
|
|
363
|
+
* 「REPL 은 readline 이 stdin 을 쥐고 있어서 위의 ask 를 그대로 못
|
|
364
|
+
* 쓴다」 고 못박아 두었다(그래서 암호는 askHidden 을 따로 만들었다).
|
|
365
|
+
* 이 한 자리만 옛 길로 남아 셋을 한꺼번에 깨고 있었다 —
|
|
366
|
+
* · readline 도 같은 바이트를 받아 그 'y' 가 큐에 쌓인다
|
|
367
|
+
* · 끝나면서 raw 모드를 꺼서 ESC·Shift+Tab 이 그때부터 안 먹는다
|
|
368
|
+
* · 여기서 Ctrl+C 를 누르면 prompt.js 의 process.exit(130) 이 돌아
|
|
369
|
+
* MCP·언어 서버·뒤에서 돌던 명령을 하나도 안 거두고 죽는다
|
|
370
|
+
*/
|
|
371
|
+
const 답 = String(await ask('띄울까요?', { def: 'y', 끝나면: 'n', 멈추면: 'n' })).trim().toLowerCase();
|
|
372
|
+
const 예 = !['n', 'no', 'ㄴ', '아니', '아니요', '아니오', '취소', '그만'].includes(답);
|
|
356
373
|
say('');
|
|
357
374
|
return 예;
|
|
358
375
|
};
|
|
@@ -381,7 +398,7 @@ export async function chatLoop(opts = {}) {
|
|
|
381
398
|
say(` ${c.gray('이어할 대화가 없습니다. 새로 시작합니다.')}`);
|
|
382
399
|
} else {
|
|
383
400
|
store = new Store(root, target);
|
|
384
|
-
const { messages:
|
|
401
|
+
const { messages: 적힌것, 못읽음 } = store.load();
|
|
385
402
|
/*
|
|
386
403
|
* 도구가 도는 중에 죽었으면 호출만 적히고 결과가 없다. 그대로 보내면
|
|
387
404
|
* 규격 서버가 400 을 내서 이어받자마자 첫 마디에서 죽는다. 손봐서 받는다.
|
|
@@ -394,6 +411,29 @@ export async function chatLoop(opts = {}) {
|
|
|
394
411
|
if (고친것) {
|
|
395
412
|
say(` ${c.gray(`중단된 도구 호출 ${고친것}개를 걷어냈습니다 — 그때 하던 일은 다시 시켜 주세요.`)}`);
|
|
396
413
|
}
|
|
414
|
+
} else {
|
|
415
|
+
/*
|
|
416
|
+
* ── 못 이어받았으면 **말한다** ──────────────────────────────────
|
|
417
|
+
*
|
|
418
|
+
* 여태 이 갈래가 통째로 없었다. `--resume 오타난아이디` 를 치면
|
|
419
|
+
* 화면에 **아무 줄도 안 나오고** 곧바로 머리말이 뜬다. 그러고는
|
|
420
|
+
* 그 아이디로 새 파일이 만들어져서, 사람이 「이어서 해줘」 라고
|
|
421
|
+
* 하면 모델은 아무것도 모른다 — 이어받은 줄 알고 말을 잇는데
|
|
422
|
+
* 상대는 처음 보는 대화다.
|
|
423
|
+
*
|
|
424
|
+
* store.load() 는 못 읽은 까닭을 이미 돌려준다(agent/store.js 의
|
|
425
|
+
* 못읽음). 그것도 여기서 버리고 있었다 — 권한 때문인지 다른
|
|
426
|
+
* 프로그램이 잡고 있어서인지 화면 어디에도 안 남았다.
|
|
427
|
+
*/
|
|
428
|
+
say('');
|
|
429
|
+
if (못읽음) {
|
|
430
|
+
say(` ${mark.warn} ${c.bold(target)} ${c.gray('— 그 대화를 못 읽었습니다')} ${c.gray('(' + 못읽음 + ')')}`);
|
|
431
|
+
} else if (고친것) {
|
|
432
|
+
say(` ${mark.warn} ${c.bold(target)} ${c.gray(`— 남은 것이 중단된 도구 호출 ${고친것}개뿐이라 이어받을 말이 없습니다.`)}`);
|
|
433
|
+
} else {
|
|
434
|
+
say(` ${mark.warn} ${c.bold(target)} ${c.gray('— 그런 대화가 없거나 비어 있습니다.')}`);
|
|
435
|
+
}
|
|
436
|
+
say(` ${c.gray('새 대화로 시작합니다. 이어할 것을 찾으려면')} ${c.cyan('/sessions')}`);
|
|
397
437
|
}
|
|
398
438
|
/*
|
|
399
439
|
* 못 박아 둔 것도 같이 되살린다 (agent/pins.js).
|
|
@@ -410,7 +450,17 @@ export async function chatLoop(opts = {}) {
|
|
|
410
450
|
}
|
|
411
451
|
if (!store) store = new Store(root);
|
|
412
452
|
store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
|
|
413
|
-
|
|
453
|
+
// 머리말보다 먼저 일어난 탈은 여기 모았다가 아래 경고 자리에서 같이 찍는다.
|
|
454
|
+
const 켤때경고 = [];
|
|
455
|
+
// 되돌리기 이력을 못 읽는다는 말은 한 판에 한 번만 한다 — 턴마다 세는 자리라
|
|
456
|
+
// 안 막으면 같은 줄이 턴 수만큼 쌓인다.
|
|
457
|
+
let 되돌리기못읽음말했나 = false;
|
|
458
|
+
/*
|
|
459
|
+
* 정리가 실패해도 켜는 것은 막지 않는다. 다만 **삼키지는 않는다.**
|
|
460
|
+
* 계속 실패하면 .deel/sessions 가 끝없이 커지는데 화면에는 영영 안 떴다.
|
|
461
|
+
* 쓰기 실패는 store 가 세어서 알리는데(아래 저장 경고) 이 자리만 옛 모양이었다.
|
|
462
|
+
*/
|
|
463
|
+
try { prune(root); } catch (e) { 켤때경고.push(`오래된 대화 정리를 못 했습니다 — ${clip(String(e?.message ?? e), 60)}`); }
|
|
414
464
|
|
|
415
465
|
/*
|
|
416
466
|
* 밖에서 붙인 도구(MCP) 서버를 띄운다.
|
|
@@ -646,6 +696,17 @@ export async function chatLoop(opts = {}) {
|
|
|
646
696
|
// 상자를 걷어내고, 사람이 보낸 글을 대화에 남긴다. 안 남기면 스크롤을
|
|
647
697
|
// 올렸을 때 답만 있고 무엇을 물었는지가 없다.
|
|
648
698
|
const 이었나 = 이어쓰기줄들 !== null;
|
|
699
|
+
/*
|
|
700
|
+
* **지우기 전에** 챙긴다.
|
|
701
|
+
*
|
|
702
|
+
* 아래 스물몇 줄 뒤에서 `이어쓴것` 을 보고 「앞줄은 이미 찍혔으니
|
|
703
|
+
* 마지막 줄만 이어 찍는다」 를 고르는데, 그 값이 바로 여기서 거짓으로
|
|
704
|
+
* 지워지고 있었다. 그래서 그 갈래는 **한 번도 안 골렸다** — 백틱으로
|
|
705
|
+
* 이어 쓰면 줄쌓기() 가 이미 찍어 둔 앞줄이 통째로 다시 찍혔다.
|
|
706
|
+
* 바로 그 자리의 주석이 「안 그러면 같은 줄이 두 번 보인다」 고
|
|
707
|
+
* 적어 둔 것인데, 코드가 정확히 그것을 하고 있었다.
|
|
708
|
+
*/
|
|
709
|
+
const 백틱으로이었나 = 이어쓴것;
|
|
649
710
|
보낼것 = 이었나 ? [...이어쓰기줄들, l].join('\n') : l;
|
|
650
711
|
이어쓰기줄들 = null; 이어쓴것 = false;
|
|
651
712
|
화면.입력지움();
|
|
@@ -666,10 +727,10 @@ export async function chatLoop(opts = {}) {
|
|
|
666
727
|
* 백틱으로 이어 쓴 경우에는 앞줄이 이미 찍혀 있으므로 마지막 줄만
|
|
667
728
|
* 이어 찍는다 — 안 그러면 같은 줄이 두 번 보인다.
|
|
668
729
|
*/
|
|
669
|
-
const 찍을것 = 남길글(
|
|
730
|
+
const 찍을것 = 남길글(백틱으로이었나 ? l : 보낼것, 붙인것들);
|
|
670
731
|
if (찍을것.trim() || 이었나) {
|
|
671
732
|
for (const [i, 한줄] of String(찍을것).split('\n').entries()) {
|
|
672
|
-
say(`${i === 0 &&
|
|
733
|
+
say(`${i === 0 && !백틱으로이었나 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(한줄)}`);
|
|
673
734
|
}
|
|
674
735
|
}
|
|
675
736
|
} else if (l.trim() || 이어쓰기줄들 !== null) {
|
|
@@ -1077,7 +1138,42 @@ export async function chatLoop(opts = {}) {
|
|
|
1077
1138
|
* y 를 쳐도 화면에 아무것도 안 나타난다 — 먹은 건지 안 먹은 건지 모른다.
|
|
1078
1139
|
*/
|
|
1079
1140
|
묻는중 = 상자쓰나 ? 앞 : null;
|
|
1080
|
-
|
|
1141
|
+
/*
|
|
1142
|
+
* 쌓아 둔 이어쓰기를 버릴 때는 **말한다.**
|
|
1143
|
+
*
|
|
1144
|
+
* SIGINT 쪽(아래)은 「이어쓰던 것을 버렸습니다」 를 반드시 찍는데 여기만
|
|
1145
|
+
* 조용했다. 일하는 도중에 줄 끝 백틱으로 여러 줄을 쌓아 두면 그 글은
|
|
1146
|
+
* 화면에 안 찍힌 채 버퍼에만 있다 — 그 사이 물음이 뜨면 통째로 사라지고,
|
|
1147
|
+
* 사람은 자기가 친 세 줄이 어디로 갔는지 알 길이 없다.
|
|
1148
|
+
*/
|
|
1149
|
+
if (이어쓰기줄들 !== null) {
|
|
1150
|
+
const 버린줄수 = 이어쓰기줄들.length;
|
|
1151
|
+
이어쓰기줄들 = null; 이어쓴것 = false;
|
|
1152
|
+
say(` ${c.gray(`쌓아 두던 ${버린줄수}줄은 버렸습니다 — 물음이 끼어들었습니다.`)}`);
|
|
1153
|
+
}
|
|
1154
|
+
/*
|
|
1155
|
+
* ── 물음이 뜨기 **전에** 쳐 둔 줄은 이 물음의 답이 아니다 ────────────
|
|
1156
|
+
*
|
|
1157
|
+
* nextLine 은 큐에 뭐가 있으면 그것부터 준다. 그래서 일하는 동안 미리
|
|
1158
|
+
* 쳐 둔 「다음은 테스트도 돌려줘」 가, 30초 뒤에 뜬 「실행할까요?」 의
|
|
1159
|
+
* 답으로 튀어나왔다. y 가 아니니 거부로 읽히고, 그 지시는 큐에서
|
|
1160
|
+
* 사라져 모델에게 **영영 안 간다.** 화면에는 사람이 답한 것과 똑같은
|
|
1161
|
+
* 모양이 남는다 — 무엇이 답으로 쓰였는지 아무 데도 안 적힌다.
|
|
1162
|
+
*
|
|
1163
|
+
* Ask 도구는 더 나쁘다. 「3번 파일도 고쳐줘」 의 앞머리 숫자를 골라진
|
|
1164
|
+
* 항목으로 읽어서, 사람이 3번을 고른 것과 똑같은 화면이 된다.
|
|
1165
|
+
*
|
|
1166
|
+
* 잠깐 빼 뒀다가 되돌려 놓는다. 버리는 것이 아니다 — 턴이 끝나면
|
|
1167
|
+
* 평소대로 처리돼야 한다.
|
|
1168
|
+
*
|
|
1169
|
+
* ── 파이프는 다르다 ────────────────────────────────────────────────
|
|
1170
|
+
*
|
|
1171
|
+
* 손으로 치는 자리가 없으면 **미리 넣는 것 말고는 답할 길이 없다.**
|
|
1172
|
+
* 답을 미리 실어 보내는 것(`echo y | deel`)이 정상 사용이고,
|
|
1173
|
+
* 여기서 그것까지 빼 두면 물음이 영영 안 끝난다. 사람이 앞에 앉아
|
|
1174
|
+
* 있을 때만 「먼저 친 줄은 답이 아니다」 가 참이다.
|
|
1175
|
+
*/
|
|
1176
|
+
const 미리쳐둔 = process.stdin.isTTY ? queue.splice(0) : [];
|
|
1081
1177
|
const 멈춤 = o.signal ?? turn?.signal ?? null;
|
|
1082
1178
|
try {
|
|
1083
1179
|
const a = await nextLine(멈춤);
|
|
@@ -1087,9 +1183,21 @@ export async function chatLoop(opts = {}) {
|
|
|
1087
1183
|
say(` ${c.gray(옮긴말('ask.stopped'))}`);
|
|
1088
1184
|
return o.멈추면 ?? '';
|
|
1089
1185
|
}
|
|
1090
|
-
|
|
1186
|
+
/*
|
|
1187
|
+
* 입력이 **끝난 것**과 사람이 그냥 Enter 를 친 것은 다르다.
|
|
1188
|
+
*
|
|
1189
|
+
* 여태 둘 다 `def` 였다. 파이프로 들어왔거나 Ctrl+D 를 누르면 아무도
|
|
1190
|
+
* 답하지 않았는데 기본값이 답으로 잡힌다 — 「나가도 될까요?」 의 def 는
|
|
1191
|
+
* 'y' 라, 사람이 한 글자도 안 친 판에서 바깥 연결이 허락된다.
|
|
1192
|
+
* 부르는 쪽이 `끝나면` 으로 그 경우를 따로 정할 수 있게 한다.
|
|
1193
|
+
*/
|
|
1194
|
+
if (a === null) return o.끝나면 ?? o.def ?? '';
|
|
1091
1195
|
return a.trim() || o.def || '';
|
|
1092
|
-
} finally {
|
|
1196
|
+
} finally {
|
|
1197
|
+
묻는중 = null;
|
|
1198
|
+
// 빼 뒀던 것을 앞에 되돌린다. 차례가 바뀌면 안 된다.
|
|
1199
|
+
if (미리쳐둔.length) queue.unshift(...미리쳐둔);
|
|
1200
|
+
}
|
|
1093
1201
|
};
|
|
1094
1202
|
|
|
1095
1203
|
/**
|
|
@@ -1188,6 +1296,16 @@ export async function chatLoop(opts = {}) {
|
|
|
1188
1296
|
loadedSkills: new Set(),
|
|
1189
1297
|
// 고친 뒤 진단을 볼지. /lsp off 로 끈다 (lsp/diag.js).
|
|
1190
1298
|
lsp: { 켬: true },
|
|
1299
|
+
/*
|
|
1300
|
+
* 종을 울릴지. `/bell on|off` 가 **이 그릇**을 고친다.
|
|
1301
|
+
*
|
|
1302
|
+
* 앞서는 명령이 설정 파일만 고쳤다. 울릴지 말지는 켤 때 한 번 읽어
|
|
1303
|
+
* 둔 값(알림.켬)이 정하므로, `/bell off` 를 쳐도 그 세션 내내 계속
|
|
1304
|
+
* 울렸다 — 화면은 「이 세션에도 바로 먹습니다」 라고 적으면서.
|
|
1305
|
+
* `/bell on` 은 더 나빴다: 그 자리에서 한 번 울려 켜졌다는 증거까지
|
|
1306
|
+
* 보여 준 다음, 다음 턴부터 안 울렸다.
|
|
1307
|
+
*/
|
|
1308
|
+
종알림: 알림,
|
|
1191
1309
|
ask,
|
|
1192
1310
|
// 암호는 여기서만 받는다. 받은 값은 도구가 쓰고 버린다 —
|
|
1193
1311
|
// 설정에도, 세션 기록에도, 감사기록에도, 명령줄에도 안 남는다.
|
|
@@ -1426,7 +1544,22 @@ export async function chatLoop(opts = {}) {
|
|
|
1426
1544
|
// --ctx 로 직접 주신 값이 있으면 안 건드린다. 사람이 고른 것을 뒤집지 않는다.
|
|
1427
1545
|
const 길이알림 = []; // 잘 된 소식
|
|
1428
1546
|
const 길이경고 = []; // 손을 봐야 하는 것
|
|
1429
|
-
|
|
1547
|
+
/*
|
|
1548
|
+
* ── 부르는 자리가 둘이라 함수로 뺀다 ──────────────────────────────────
|
|
1549
|
+
*
|
|
1550
|
+
* 바깥 연결에 아직 허락을 안 준 판에서는 이 두드림이 **우리 문지기에**
|
|
1551
|
+
* 막힌다(safety/network.js). 아래 「나가도 될까요?」 에 답해야 문이 열리는데,
|
|
1552
|
+
* 그 물음은 머리말 다음에 있어야 해서 여기보다 한참 뒤다.
|
|
1553
|
+
*
|
|
1554
|
+
* 그래서 여태 바깥 프로필을 처음 붙이는 사람은 **언제나** 이 줄을 봤다:
|
|
1555
|
+
* ⚠ 컨텍스트를 서버가 안 알려줍니다 — 우선 32,768 으로 잡았습니다
|
|
1556
|
+
* 서버는 이 물음을 받아 본 적도 없다. 그리고 허락한 뒤에도 다시 안 물어봐서,
|
|
1557
|
+
* 128k 짜리 모델을 그 판 내내 32,768 로 알고 쓴다 — 화면 어디에도 그 말이 없다.
|
|
1558
|
+
*
|
|
1559
|
+
* 매개변수 이름을 바깥 것과 똑같이 둔 것은 일부러다. 몸통이 그대로 먹는다.
|
|
1560
|
+
*/
|
|
1561
|
+
const 길이알아내기 = async (길이알림, 길이경고) => {
|
|
1562
|
+
if (opts.ctx != null) return;
|
|
1430
1563
|
// 이 컴퓨터 안의 서버면 눈 깜짝할 새다. 사내 게이트웨이는 몇 초 걸릴 수 있어
|
|
1431
1564
|
// 무슨 일이 일어나는 중인지 알려 준다 — 멈춘 것처럼 보이면 안 된다.
|
|
1432
1565
|
화면.돌리기(옮긴말('run.ctxProbe'));
|
|
@@ -1483,9 +1616,36 @@ export async function chatLoop(opts = {}) {
|
|
|
1483
1616
|
+ `\n 너무 길면 서버가 알려 주는 값으로 저절로 맞춥니다. 아는 값이 있으면 ${c.cyan('/ctx 8192')} 처럼 직접 정하세요`);
|
|
1484
1617
|
}
|
|
1485
1618
|
} else if (prof.ctx == null) {
|
|
1486
|
-
|
|
1619
|
+
/*
|
|
1620
|
+
* **왜** 못 알아냈는지까지 적는다.
|
|
1621
|
+
*
|
|
1622
|
+
* probeCtx 는 그 까닭을 이미 들고 온다(backend/ctxsize.js 의 why) —
|
|
1623
|
+
* 「나갈 허락이 없다」 · 「응답은 왔는데 길이를 안 준다」 · 「아무 응답도
|
|
1624
|
+
* 없다」 는 사람이 할 일이 셋 다 다르다. 여태 이 자리에서 그걸 버리고
|
|
1625
|
+
* 늘 「서버가 안 알려줍니다」 한 문장으로 뭉갰다.
|
|
1626
|
+
*/
|
|
1627
|
+
길이경고.push(`컨텍스트를 못 알아냈습니다 ${c.gray('(' + (r?.why ?? '까닭 모름') + ')')} — 우선 ${CTX_DEFAULT.toLocaleString()} 으로 잡았습니다. ${c.cyan('/ctx 655360')} 처럼 직접 지정하세요`);
|
|
1628
|
+
} else if (r?.why && !r?.tried?.some((t) => t.ok)) {
|
|
1629
|
+
/*
|
|
1630
|
+
* 프로필에 값이 적혀 있으면 **아무 말도 안 하던** 자리다.
|
|
1631
|
+
*
|
|
1632
|
+
* 이 블록 맨 위에 적어 둔 약속이 「켤 때마다 서버에 물어본다. 저장된
|
|
1633
|
+
* 값을 그대로 믿지 않는다」 인데, 못 물어본 판에서는 그 약속이 그냥
|
|
1634
|
+
* 거짓이 된다 — 머리말에는 저장된 숫자가 **방금 서버에서 확인한 값**
|
|
1635
|
+
* 처럼 뜬다. 서버에서 창을 줄여 놨어도 알 길이 없다.
|
|
1636
|
+
*
|
|
1637
|
+
* 서버가 **응답은 했는데 길이만 안 주는** 흔한 판에서는 조용히 있는다
|
|
1638
|
+
* (tried 에 ok 가 하나라도 있으면 그 경우다). 그건 늘 그런 것이라,
|
|
1639
|
+
* 켤 때마다 경고를 붙이면 곧 아무도 안 읽는다. 여기서 말하는 것은
|
|
1640
|
+
* **아무 데서도 대답을 못 받은** 판뿐이다.
|
|
1641
|
+
*/
|
|
1642
|
+
길이경고.push(`컨텍스트를 서버에 못 물어봤습니다 ${c.gray('(' + r.why + ')')}`
|
|
1643
|
+
+ ` — 프로필에 적힌 ${c.white(Number(prof.ctx).toLocaleString())} 을 그대로 씁니다. 서버에서 바뀌었으면 ${c.cyan('/ctx auto')}`);
|
|
1487
1644
|
}
|
|
1488
|
-
}
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
// 허락을 물어야 하는 연결이면 여기서 안 두드린다 — 묻고 난 뒤에 부른다.
|
|
1648
|
+
if (!나감.물어볼까) await 길이알아내기(길이알림, 길이경고);
|
|
1489
1649
|
|
|
1490
1650
|
/*
|
|
1491
1651
|
* ── 켤 때 도는 글자 모션 ──────────────────────────────────────────────
|
|
@@ -1547,6 +1707,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1547
1707
|
}
|
|
1548
1708
|
if (!conn.tools) warn.push(옮긴말('run.noTools'));
|
|
1549
1709
|
if (!conn.streaming) warn.push(옮긴말('run.noStream'));
|
|
1710
|
+
warn.push(...켤때경고);
|
|
1550
1711
|
warn.push(...길이경고);
|
|
1551
1712
|
// 잘 된 것은 경고 표시를 달지 않는다. ⚠ 가 붙으면 뭘 고쳐야 하나 싶어진다.
|
|
1552
1713
|
for (const l of 길이알림) say(` ${mark.ok} ${c.gray(l)}`);
|
|
@@ -1566,6 +1727,22 @@ export async function chatLoop(opts = {}) {
|
|
|
1566
1727
|
* 뒤집으면서 쓰던 사람을 안 깨뜨리는 길이 이것이다. 막는 것이 목적이 아니라
|
|
1567
1728
|
* **모르고 나가는 것**을 없애는 것이 목적이다.
|
|
1568
1729
|
*/
|
|
1730
|
+
/*
|
|
1731
|
+
* 여기서 나가도 **띄운 것은 거둔다.**
|
|
1732
|
+
*
|
|
1733
|
+
* 이 자리의 `rl.close(); return 0;` 은 맨 아래 정리 블록을 통째로 건너뛰었다.
|
|
1734
|
+
* MCP 서버는 이미 떠 있다(위의 다붙이기) — 남의 프로세스가 그대로 남는다.
|
|
1735
|
+
* 「deel 을 껐는데도 그 서버가 계속 돌고 있게 된다」 는 바로 그 자리의
|
|
1736
|
+
* 주석이 없애겠다고 적어 둔 것인데, 이 길로 나가면 그대로 일어났다.
|
|
1737
|
+
*/
|
|
1738
|
+
const 일찍끝내기 = (코드) => {
|
|
1739
|
+
rl.close();
|
|
1740
|
+
for (const 서버 of mcp붙임.서버들) 서버.닫기();
|
|
1741
|
+
언어서버다끄기().catch(() => {});
|
|
1742
|
+
화면.close();
|
|
1743
|
+
return 코드;
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1569
1746
|
if (나감.물어볼까) {
|
|
1570
1747
|
const 어디 = (() => { try { return new URL(conn.base).host; } catch { return conn.base; } })();
|
|
1571
1748
|
say('');
|
|
@@ -1574,14 +1751,39 @@ export async function chatLoop(opts = {}) {
|
|
|
1574
1751
|
say(` ${c.gray('보내는 것 — 시킨 말, 그리고 모델이 읽은 파일의 내용입니다.')}`);
|
|
1575
1752
|
say(` ${c.gray('로컬 모델(127.0.0.1)이나 사내망 주소였다면 이 물음이 안 나옵니다.')}`);
|
|
1576
1753
|
say('');
|
|
1577
|
-
|
|
1754
|
+
|
|
1755
|
+
/*
|
|
1756
|
+
* ── 답할 사람이 없으면 **묻지 않는다** ────────────────────────────
|
|
1757
|
+
*
|
|
1758
|
+
* 이 자리가 이 프로그램이 파는 단 하나의 약속이다 — 「모르고 나가는
|
|
1759
|
+
* 것을 없앤다」. 그런데 파이프로 들어오면 그 약속이 조용히 꺼졌다:
|
|
1760
|
+
*
|
|
1761
|
+
* echo "이 폴더 요약해줘" | deel
|
|
1762
|
+
* › 나가도 될까요? ⏎ 허락하고 기억 · n 그만 [y] 이 폴더 요약해줘
|
|
1763
|
+
* 허락을 적어 뒀습니다 — 이 프로필은 다음부터 안 묻습니다.
|
|
1764
|
+
*
|
|
1765
|
+
* 사람은 한 글자도 답하지 않았다. 시킨 말이 답으로 먹혔고(그래서
|
|
1766
|
+
* 모델에게 가지도 않았다), online: true 가 설정 파일에 박혀 그 프로필은
|
|
1767
|
+
* 앞으로 영영 안 묻는다. 빈 파이프여도 같다 — EOF 가 def 'y' 로 잡혔다.
|
|
1768
|
+
*
|
|
1769
|
+
* 손으로 켠 판에서만 묻는다. 그 밖에는 안 나간다 — 미리 허락해 두는
|
|
1770
|
+
* 길(설정의 online, --online)이 이미 있고, 그 길은 사람이 마음먹고
|
|
1771
|
+
* 지나가는 자리다.
|
|
1772
|
+
*/
|
|
1773
|
+
if (!process.stdin.isTTY) {
|
|
1774
|
+
say(` ${mark.warn} ${c.gray('파이프로 들어와 있어 물어볼 수가 없습니다 — 나가지 않았습니다.')}`);
|
|
1775
|
+
say(` ${c.gray('내보내려면 먼저 손으로 한 번 켜서 허락하거나,')} ${c.cyan('--online')} ${c.gray('을 붙이세요.')}`);
|
|
1776
|
+
return 일찍끝내기(0);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// 멈춤(ESC)도 입력 끝(Ctrl+D)도 **허락이 아니다.** def 로 물러나면 둘 다 'y' 가 된다.
|
|
1780
|
+
const 답 = String(await ask(`나가도 될까요? ${c.gray('⏎ 허락하고 기억 · n 그만')}`, { def: 'y', 끝나면: 'n', 멈추면: 'n' }))
|
|
1578
1781
|
.trim().toLowerCase();
|
|
1579
1782
|
if (['n', 'no', 'ㄴ', '아니', '아니요', '아니오', '취소', '그만'].includes(답)) {
|
|
1580
1783
|
say('');
|
|
1581
1784
|
say(` ${c.gray('나가지 않았습니다. 아무것도 안 보냈습니다.')}`);
|
|
1582
1785
|
say(` ${c.gray('이 컴퓨터 안의 모델을 붙이려면')} ${c.cyan('deel setup')} ${c.gray('또는')} ${c.cyan('deel scan --save')} ${c.gray('를 쓰세요.')}`);
|
|
1583
|
-
|
|
1584
|
-
return 0;
|
|
1786
|
+
return 일찍끝내기(0);
|
|
1585
1787
|
}
|
|
1586
1788
|
prof.online = true;
|
|
1587
1789
|
try {
|
|
@@ -1593,6 +1795,18 @@ export async function chatLoop(opts = {}) {
|
|
|
1593
1795
|
say(` ${mark.warn} ${c.gray(`허락을 저장하지 못했습니다 (${e.message}). 이번에만 나갑니다.`)}`);
|
|
1594
1796
|
}
|
|
1595
1797
|
allowEndpoint(conn.base);
|
|
1798
|
+
|
|
1799
|
+
/*
|
|
1800
|
+
* 이제야 문이 열렸다. 컨텍스트 길이는 **여기서** 알아본다.
|
|
1801
|
+
*
|
|
1802
|
+
* 위쪽 자리에서 두드렸다면 방금 연 이 문 때문에 전부 막혀 있었다.
|
|
1803
|
+
* 늦게 찍히는 만큼 자리를 따로 잡아 준다 — 머리말에 섞여 들어가면
|
|
1804
|
+
* 방금 답한 물음과 아무 상관 없는 줄로 보인다.
|
|
1805
|
+
*/
|
|
1806
|
+
const 늦은알림 = []; const 늦은경고 = [];
|
|
1807
|
+
await 길이알아내기(늦은알림, 늦은경고);
|
|
1808
|
+
for (const l of 늦은알림) say(` ${mark.ok} ${c.gray(l)}`);
|
|
1809
|
+
for (const w of 늦은경고) say(` ${mark.warn} ${c.gray(w)}`);
|
|
1596
1810
|
}
|
|
1597
1811
|
|
|
1598
1812
|
/*
|
|
@@ -1694,12 +1908,55 @@ export async function chatLoop(opts = {}) {
|
|
|
1694
1908
|
text = line.trim();
|
|
1695
1909
|
if (!text) continue;
|
|
1696
1910
|
if (예약이었나 && 상자쓰나) {
|
|
1911
|
+
/*
|
|
1912
|
+
* ── 통째로 쏟지 않는다 ──────────────────────────────────────────
|
|
1913
|
+
*
|
|
1914
|
+
* 큐에 든 것은 **펼쳐진 원문**이다(위 붙임펼치기). 여기서 그대로
|
|
1915
|
+
* say 하면 도구가 도는 사이에 붙여넣은 50줄이 그 자리에 쏟아진다.
|
|
1916
|
+
* 기다릴 때 보낸 것은 남길글() 로 `[붙여넣기 #1 · 47줄 · 2.1KB]` 로
|
|
1917
|
+
* 줄여 남기는데 이 길만 옛 모양이었다.
|
|
1918
|
+
*
|
|
1919
|
+
* 넓이도 지켜야 한다. 상자는 아래 몇 줄을 제 자리로 잡아 두고 거기에
|
|
1920
|
+
* 다시 그리는데, 터미널보다 넓은 줄을 밀어 넣으면 그 줄이 접히면서
|
|
1921
|
+
* 줄 셈이 어긋나고 위쪽 대화를 갉아먹는다.
|
|
1922
|
+
*/
|
|
1923
|
+
const 폭 = Math.max(20, (process.stdout.columns ?? 80) - 8);
|
|
1924
|
+
const 줄들 = String(text).split('\n');
|
|
1925
|
+
const 넘치나 = 줄들.length > 붙임안쪽최대;
|
|
1926
|
+
const 보일것 = 넘치나 ? 줄들.slice(0, 2) : 줄들;
|
|
1697
1927
|
화면.입력지움();
|
|
1698
|
-
|
|
1928
|
+
for (const [i, 한줄] of 보일것.entries()) {
|
|
1929
|
+
say(`${i === 0 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(clip(한줄, 폭))}`
|
|
1930
|
+
+ `${i === 0 ? ` ${c.gray('(미리 쳐 둔 것)')}` : ''}`);
|
|
1931
|
+
}
|
|
1932
|
+
if (넘치나) {
|
|
1933
|
+
say(` ${c.gray(`… 그 밖에 ${줄들.length - 2}줄 — 통째로 보냈습니다.`)}`);
|
|
1934
|
+
}
|
|
1699
1935
|
}
|
|
1700
1936
|
}
|
|
1701
1937
|
|
|
1702
|
-
|
|
1938
|
+
/*
|
|
1939
|
+
* ── 명령 하나가 대화를 끝내면 안 된다 ──────────────────────────────
|
|
1940
|
+
*
|
|
1941
|
+
* 여기는 어떤 try 안에도 없었다. 슬래시 명령 안에서 파일을 쓰다 던지면
|
|
1942
|
+
* (`.deel` 이 읽기 전용이거나 디스크가 찼을 때 writeFileSync 가 그렇다)
|
|
1943
|
+
* 그대로 위로 새어 나가 **프로세스가 끝났다.** `/memory 어쩌구` 한 줄에
|
|
1944
|
+
* 대화가 통째로 사라지는 것이다 — 오간 말도, 이어 갈 자리도 같이.
|
|
1945
|
+
*
|
|
1946
|
+
* 보이는 실패이긴 하지만 대가가 대화 전체다. 여기서 받아 한 줄 적고
|
|
1947
|
+
* 이어 간다. 무엇 때문인지도 같이 적는다 — 안 적으면 그 사람은 명령이
|
|
1948
|
+
* 왜 안 먹는지 영영 모른다.
|
|
1949
|
+
*/
|
|
1950
|
+
let cmd;
|
|
1951
|
+
try {
|
|
1952
|
+
cmd = await handle(text, session, ctx);
|
|
1953
|
+
} catch (e) {
|
|
1954
|
+
say('');
|
|
1955
|
+
say(` ${c.red('✗')} ${c.gray('명령을 처리하다 막혔습니다')} ${c.gray('— ' + clip(String(e?.message ?? e), 90))}`);
|
|
1956
|
+
say(` ${c.gray('대화는 그대로입니다. 다른 명령을 치거나 그냥 이어서 말씀하세요.')}`);
|
|
1957
|
+
say('');
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1703
1960
|
if (cmd.exit) break;
|
|
1704
1961
|
if (cmd.handled) continue;
|
|
1705
1962
|
const toSend = cmd.text ?? text; // 슬래시 명령이면 펼쳐진 내용을 보낸다
|
|
@@ -1808,6 +2065,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1808
2065
|
// 이 턴이 탈 없이 끝났나. 끊겼거나 터진 뒤에 승인 창을 띄우면 안 된다 —
|
|
1809
2066
|
// 계획이 반만 나온 것을 두고 "이대로 진행할까요?" 를 묻는 꼴이 된다.
|
|
1810
2067
|
let 턴탈났나 = false;
|
|
2068
|
+
// 이 턴에 TodoWrite 가 실제로 돌았나. ctx.todos 는 세션 내내 살아 있어서
|
|
2069
|
+
// 이걸 안 세면 지난 턴의 남은 할 일을 이번 계획으로 착각한다 (아래 계획 승인).
|
|
2070
|
+
let 이번턴할일적었나 = false;
|
|
1811
2071
|
/*
|
|
1812
2072
|
* 걸음을 다 써서 중간에 끊긴 턴인가. 끊겼을 때 남은 할 일을 여기 담는다.
|
|
1813
2073
|
*
|
|
@@ -2014,6 +2274,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2014
2274
|
// 같이 돈 것은 이름을 다시 적어 준다. 안 그러면 어느 결과인지 모른다.
|
|
2015
2275
|
if (ev.parallel || ev.showLabel) say(` ${toolLabel(ev.name, ev.args)}`);
|
|
2016
2276
|
if (ev.name === 'TodoWrite' && ev.result?.todos) {
|
|
2277
|
+
이번턴할일적었나 = true;
|
|
2017
2278
|
// 사무실 화이트보드도 이걸 본다. 할 일은 세션이 아니라 턴 문맥에
|
|
2018
2279
|
// 있어서 상자가 스스로 못 읽는다 — 아는 자리에서 넣어 준다.
|
|
2019
2280
|
화면.할일갱신(ev.result.todos);
|
|
@@ -2051,10 +2312,24 @@ export async function chatLoop(opts = {}) {
|
|
|
2051
2312
|
* 다른 일이다. 화면에 초록 ✓ 가 없으면 아직 아무도 안 돌려 본
|
|
2052
2313
|
* 것이고, 그 구분이 상태줄에 있어야 사람이 속지 않는다.
|
|
2053
2314
|
*/
|
|
2054
|
-
|
|
2315
|
+
/*
|
|
2316
|
+
* ── 안 돈 것을 돌았다고 세지 않는다 ──────────────────────────
|
|
2317
|
+
*
|
|
2318
|
+
* `ev.result` 만 보면 `{error:'거부됨'}` · `{error:'모르는 도구'}`
|
|
2319
|
+
* 에도 돈횟수 가 오른다. 상태줄은 돈횟수 가 0 이 아니고 탈 이
|
|
2320
|
+
* 없으면 **초록 ✓0** 을 그리는데(ui/status.js), 그 표시의 뜻은
|
|
2321
|
+
* 같은 파일이 「초록 ✓ 가 없으면 아직 아무도 안 돌려 본 것이다」
|
|
2322
|
+
* 라고 못박아 두었다. 거부된 Verify 가 초록 ✓ 로 뜨면 그 문장이
|
|
2323
|
+
* 거꾸로 선다 — 안 돌려 본 것을 돌려 봤다고 읽게 된다.
|
|
2324
|
+
*
|
|
2325
|
+
* 못 확인한 것도 센다. 도구 한 줄에서는 노랑으로 보여 주면서
|
|
2326
|
+
* 쌓는 자리에서만 사라지고 있었다.
|
|
2327
|
+
*/
|
|
2328
|
+
if (ev.name === 'Verify' && ev.result && !ev.result.error) {
|
|
2055
2329
|
session.검증.돈횟수 += 1;
|
|
2056
2330
|
session.검증.확인 += ev.result.확인됨 ?? 0;
|
|
2057
2331
|
session.검증.탈 += ev.result.탈 ?? 0;
|
|
2332
|
+
session.검증.못확인 = (session.검증.못확인 ?? 0) + (ev.result.못확인 ?? 0);
|
|
2058
2333
|
}
|
|
2059
2334
|
if (ev.result?.되돌릴것?.length) {
|
|
2060
2335
|
const 것들 = ev.result.되돌릴것;
|
|
@@ -2354,7 +2629,24 @@ export async function chatLoop(opts = {}) {
|
|
|
2354
2629
|
* (오류로 끝나면 뒤에 done 이 없다), 무엇이 잘렸는지 보라고 띄운
|
|
2355
2630
|
* 경고가 정작 그 답의 마지막 줄보다 **위에** 찍힌다.
|
|
2356
2631
|
*/
|
|
2632
|
+
/*
|
|
2633
|
+
* ── 이 넷도 「탈 없이 끝난 턴」 이 아니다 ────────────────────────
|
|
2634
|
+
*
|
|
2635
|
+
* 아래 계획 승인은 `계획승인받나 && !턴탈났나` 로 걸린다. 그 규칙을
|
|
2636
|
+
* 적어 둔 주석은 「계획이 반만 나온 것을 두고 이대로 진행할까요? 를
|
|
2637
|
+
* 묻는 꼴이 된다」 였는데, 정작 **계획이 반만 나오는 대표 사건 넷**이
|
|
2638
|
+
* 이 깃발을 안 세우고 있었다:
|
|
2639
|
+
* capped — 출력 상한에서 글이 잘렸다
|
|
2640
|
+
* cutoff — 서버가 끝난 까닭도 없이 흐름을 멈췄다
|
|
2641
|
+
* limit — 걸음 수를 다 썼다
|
|
2642
|
+
* refusal — 모델이 거절했다. 계획이 아예 없다
|
|
2643
|
+
* 넷 다 for-await 는 예외 없이 끝나므로 깃발은 거짓인 채 남았다.
|
|
2644
|
+
* 그래서 3단계에서 잘린 반쪽에 `⏎ 그대로 진행` 이 뜨고, 창 제목은
|
|
2645
|
+
* 「끝남」 이었다. limit 은 한 술 더 떠서, 이 갈래가 이겨 버리는 바람에
|
|
2646
|
+
* 「끊긴 자리에서 이어서」 물음(else if)이 아예 안 돌았다.
|
|
2647
|
+
*/
|
|
2357
2648
|
case 'capped':
|
|
2649
|
+
턴탈났나 = true;
|
|
2358
2650
|
clearThinking();
|
|
2359
2651
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
2360
2652
|
say(` ${mark.warn} ${c.gray(`답이 ${c.white(ev.cap.toLocaleString())} 토큰에서 잘렸습니다`)}`
|
|
@@ -2369,6 +2661,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2369
2661
|
* /out 을 권하면 안 된다 — 상한 얘기가 아니라 연결 얘기다.
|
|
2370
2662
|
*/
|
|
2371
2663
|
case 'cutoff':
|
|
2664
|
+
턴탈났나 = true;
|
|
2372
2665
|
clearThinking();
|
|
2373
2666
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
2374
2667
|
if (ev.멎은초) {
|
|
@@ -2388,6 +2681,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2388
2681
|
break;
|
|
2389
2682
|
|
|
2390
2683
|
case 'limit':
|
|
2684
|
+
턴탈났나 = true;
|
|
2391
2685
|
끊긴할일 = ev.남은할일 ?? [];
|
|
2392
2686
|
say('');
|
|
2393
2687
|
say(` ${mark.warn} 도구 호출 ${ev.steps}회에서 멈췄습니다.`);
|
|
@@ -2479,10 +2773,25 @@ export async function chatLoop(opts = {}) {
|
|
|
2479
2773
|
}
|
|
2480
2774
|
break;
|
|
2481
2775
|
|
|
2776
|
+
/*
|
|
2777
|
+
* 훅이 막았다. **왜 막았는지**까지 적는다.
|
|
2778
|
+
*
|
|
2779
|
+
* 여태 `ev.말` 을 안 썼다. 거기에는 safety/hooks.js 의 막힘말 이
|
|
2780
|
+
* 들어 있고, 그 안에 두 가지가 담긴다 — 훅이 뱉은 진짜 사유와,
|
|
2781
|
+
* 「훅을 못 돌렸습니다 — <오류>」·「훅이 <코드> 로 끝났습니다」.
|
|
2782
|
+
* 그걸 버리면 화면은 `✗ 훅이 막았습니다 (dlp-check)` 한 줄뿐이라,
|
|
2783
|
+
* 사내 정책이 진짜로 막은 것인지 훅 파일이 없어져 못 돈 것인지
|
|
2784
|
+
* 구별할 수가 없다. 뒤엣것은 사람이 고칠 수 있는 탈인데도.
|
|
2785
|
+
*/
|
|
2482
2786
|
case 'hook_block':
|
|
2483
2787
|
clearThinking();
|
|
2484
2788
|
say('');
|
|
2485
2789
|
say(` ${c.yellow('✗')} ${c.white(옮긴말('ev.hookBlock'))} ${c.gray(`(${ev.훅?.이름 ?? ev.훅?.명령 ?? ''})`)}`);
|
|
2790
|
+
if (ev.말) {
|
|
2791
|
+
for (const 한줄 of String(ev.말).split('\n').slice(0, 4)) {
|
|
2792
|
+
if (한줄.trim()) say(` ${c.gray(clip(한줄.trim(), 88))}`);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2486
2795
|
break;
|
|
2487
2796
|
|
|
2488
2797
|
case 'nudge':
|
|
@@ -2502,6 +2811,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2502
2811
|
* 또 거절이고, 요금만 두 배가 된다. 거절은 거절이라고 적는다.
|
|
2503
2812
|
*/
|
|
2504
2813
|
case 'refusal': {
|
|
2814
|
+
턴탈났나 = true;
|
|
2505
2815
|
clearThinking();
|
|
2506
2816
|
// 아래에서 볼 값이라 **지우기 전에** 챙긴다. 순서를 놓치면
|
|
2507
2817
|
// `!streamed` 가 늘 참이 되어 화면에 이미 있는 답을 또 찍는다.
|
|
@@ -2561,6 +2871,10 @@ export async function chatLoop(opts = {}) {
|
|
|
2561
2871
|
// 중간에 끊겨도 자리는 비운다. 안 비우면 다음 턴이 시작될 때까지
|
|
2562
2872
|
// 아무도 안 도는 방에 사람이 앉아 있다.
|
|
2563
2873
|
화면.함께갱신(0);
|
|
2874
|
+
// 화면만 비우면 안 된다. 세는 값도 같이 내린다 — 하위 작업이 도는 중에
|
|
2875
|
+
// 턴이 터지면 task_done 이 안 오고, 그 1 이 남아서 다음 턴의 첫 하위
|
|
2876
|
+
// 작업이 2 로 뜬다. 사무실에 앉은 사람 수가 실제와 어긋난다.
|
|
2877
|
+
도는하위 = 0;
|
|
2564
2878
|
화면.하위갱신(0);
|
|
2565
2879
|
clearInterval(제목시계);
|
|
2566
2880
|
/*
|
|
@@ -2575,7 +2889,24 @@ export async function chatLoop(opts = {}) {
|
|
|
2575
2889
|
* 다시 그려지고, 거기서 이력 파일을 열면 타이핑이 끊긴다. 턴 경계에서
|
|
2576
2890
|
* 한 번 세어 두면 화면은 그 숫자만 읽으면 된다.
|
|
2577
2891
|
*/
|
|
2578
|
-
|
|
2892
|
+
/*
|
|
2893
|
+
* 못 세면 **옛 숫자를 지운다.**
|
|
2894
|
+
*
|
|
2895
|
+
* 여태 `catch {}` 라 직전 값이 그대로 남았고, 상태줄은 계속 `↩ 3` 을
|
|
2896
|
+
* 그렸다. 그 숫자를 status.js 는 「안전망의 잔량」 이라고 부른다 —
|
|
2897
|
+
* 이력을 못 읽는 상태는 곧 /undo 도 못 하는 상태인데, 화면에는 되돌릴
|
|
2898
|
+
* 것이 셋 남았다고 떠 있는 셈이다. 한 번은 말하고, 숫자는 내린다.
|
|
2899
|
+
*/
|
|
2900
|
+
try {
|
|
2901
|
+
session.되돌릴턴 = ctx.history.turns().length;
|
|
2902
|
+
} catch (e) {
|
|
2903
|
+
session.되돌릴턴 = 0;
|
|
2904
|
+
if (!되돌리기못읽음말했나) {
|
|
2905
|
+
되돌리기못읽음말했나 = true;
|
|
2906
|
+
say(` ${mark.warn} ${c.gray('되돌리기 이력을 못 읽었습니다')} ${c.gray('— ' + clip(String(e?.message ?? e), 60))}`);
|
|
2907
|
+
say(` ${c.gray('이 판에서는')} ${c.cyan('/undo')} ${c.gray('가 안 될 수 있습니다.')}`);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2579
2910
|
if (알릴까({ 걸린밀리초: Date.now() - started, 켬: 알림.켬 })) 종();
|
|
2580
2911
|
창제목(제목글(턴탈났나 ? '탈남' : '끝남', { 폴더: 알림.폴더 }));
|
|
2581
2912
|
turn = null;
|
|
@@ -2644,7 +2975,23 @@ export async function chatLoop(opts = {}) {
|
|
|
2644
2975
|
* 애초에 계획 같은 걸 안 보려 든다.
|
|
2645
2976
|
*/
|
|
2646
2977
|
if (계획승인받나 && !턴탈났나) {
|
|
2647
|
-
|
|
2978
|
+
/*
|
|
2979
|
+
* ── 이번 턴에 적은 계획만 이번 계획이다 ──────────────────────────
|
|
2980
|
+
*
|
|
2981
|
+
* `ctx` 는 켤 때 한 번 만들어 세션 내내 쓰이고, `ctx.todos` 를 비우는
|
|
2982
|
+
* 자리는 어디에도 없다. 그래서 이런 판이 난다:
|
|
2983
|
+
*
|
|
2984
|
+
* 턴 A 로그인 작업이 걸음 상한에 걸려 안 끝난 할 일 둘이 남는다
|
|
2985
|
+
* 턴 B 「결제 모듈 설계하고 만들어줘」 → 겹침이라 계획 승인이 켜진다
|
|
2986
|
+
* 모델은 TodoWrite 없이 산문으로만 설계를 낸다
|
|
2987
|
+
* 화면 ┌ 계획 / 1. 로그인 폼 만들기 / 2. 세션 쿠키 / ⏎ 그대로 진행
|
|
2988
|
+
*
|
|
2989
|
+
* ⏎ 를 누르면 그 **지난 턴 목록**이 「방금 낸 계획」 이라는 이름표를
|
|
2990
|
+
* 달고 모델에게 실려 나간다. 이 파일은 「TodoWrite 를 안 쓴 계획도
|
|
2991
|
+
* 있다」 를 이미 대비해 뒀지만(할일.length === 0 갈래), 목록이 **비지
|
|
2992
|
+
* 않고 옛것으로 차 있는** 경우가 그 그물을 통과했다.
|
|
2993
|
+
*/
|
|
2994
|
+
const 할일 = 이번턴할일적었나 ? (ctx.todos ?? []).filter((t) => t.state !== 'done') : [];
|
|
2648
2995
|
say('');
|
|
2649
2996
|
계획상자(할일);
|
|
2650
2997
|
say('');
|
|
@@ -2723,7 +3070,15 @@ export async function chatLoop(opts = {}) {
|
|
|
2723
3070
|
? 옮긴말('plan.replanStep', { 단계: 계획답.단계 })
|
|
2724
3071
|
: 옮긴말('plan.replan'))}`);
|
|
2725
3072
|
}
|
|
2726
|
-
|
|
3073
|
+
/*
|
|
3074
|
+
* 빈 배열은 「이어갈 것이 있다」 가 아니다.
|
|
3075
|
+
*
|
|
3076
|
+
* `끊긴할일 = ev.남은할일 ?? []` 라 할 일이 0개인 채 걸음 상한에
|
|
3077
|
+
* 걸려도 여기가 참이 됐다. 그러면 물음이 뜨고, ⏎ 한 번에 「하던
|
|
3078
|
+
* 자리에서 이어서 해라」 로 모델을 한 판 더 부른다 — 이어갈 자리가
|
|
3079
|
+
* 없는데.
|
|
3080
|
+
*/
|
|
3081
|
+
} else if (끊긴할일?.length) {
|
|
2727
3082
|
/*
|
|
2728
3083
|
* ── 끊긴 자리에서 ⏎ 하나로 잇는다 ────────────────────────────────
|
|
2729
3084
|
*
|
package/src/tools/index.js
CHANGED
|
@@ -2769,7 +2769,17 @@ export async function runTool(name, args, ctx) {
|
|
|
2769
2769
|
// 섞이면 "이 폴더 밖은 못 건드린다" 는 말이 거짓이 된다.
|
|
2770
2770
|
if (name.startsWith('mcp__')) return await runMcpTool(name, args, ctx);
|
|
2771
2771
|
|
|
2772
|
-
|
|
2772
|
+
/*
|
|
2773
|
+
* 있는 도구인지는 hasOwn 으로만 묻는다.
|
|
2774
|
+
*
|
|
2775
|
+
* 이름은 **모델이 지어서 보낸 것**이다. `constructor` · `toString` 으로
|
|
2776
|
+
* 오면 `TOOLS[name]` 이 물려받은 함수라 참이 되어 이 관문을 통과하고,
|
|
2777
|
+
* 조금 아래 `t.run(...)` 에서 터진다. 화면에 남는 말은 「모르는 도구:
|
|
2778
|
+
* toString」 이 아니라 `t.run is not a function` 이다 — 사람도 모델도
|
|
2779
|
+
* 무엇이 잘못됐는지 알 수 없는 글이고, 모델은 고칠 실마리가 없으니
|
|
2780
|
+
* 같은 이름으로 또 부른다.
|
|
2781
|
+
*/
|
|
2782
|
+
const t = Object.hasOwn(TOOLS, name) ? TOOLS[name] : null;
|
|
2773
2783
|
if (!t) return { error: `모르는 도구: ${name}` };
|
|
2774
2784
|
|
|
2775
2785
|
/*
|