deel-local-cli 1.17.4 → 1.17.6
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 +5 -3
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/acp/serve.js +3 -1
- package/src/agent/loop.js +142 -16
- package/src/agent/models.js +3 -1
- package/src/agent/modes.js +4 -1
- package/src/agent//353/213/250/352/263/204.js +133 -0
- package/src/backend/adapter.js +75 -1
- package/src/backend/http.js +51 -0
- package/src/commands.js +3 -1
- package/src/i18n/en.js +6 -0
- package/src/i18n/ja.js +6 -0
- package/src/i18n/ko.js +6 -0
- package/src/i18n/zh.js +6 -0
- package/src/oneshot.js +25 -2
- package/src/repl.js +45 -1
package/README.ko.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/ko/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -268,7 +268,7 @@ deel --offline
|
|
|
268
268
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
269
269
|
|
|
270
270
|
```
|
|
271
|
-
deel 1.17.
|
|
271
|
+
deel 1.17.6 ⌂ 이 안
|
|
272
272
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
273
273
|
```
|
|
274
274
|
|
|
@@ -1185,7 +1185,7 @@ deel stats # 이 폴더에서 실제로 무엇을 했나 (.de
|
|
|
1185
1185
|
## 개발
|
|
1186
1186
|
|
|
1187
1187
|
```bash
|
|
1188
|
-
npm test 전체 검증 (
|
|
1188
|
+
npm test 전체 검증 (8,011항목 — 몇몇은 터미널에 따라 갈립니다)
|
|
1189
1189
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
1190
1190
|
npm run verify 반입·통신 검증만
|
|
1191
1191
|
npm run bench 편집 성공률 측정
|
|
@@ -1254,6 +1254,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1254
1254
|
|
|
1255
1255
|
| 판 | 무엇이 바뀌었나 |
|
|
1256
1256
|
|---|---|
|
|
1257
|
+
| [1.17.6](docs/ko/releases/1.17.md#1176) | 살아 있다는 신호만 오고 내용이 안 오면, 여태 영원히 기다렸습니다 |
|
|
1258
|
+
| [1.17.5](docs/ko/releases/1.17.md#1175) | 한 턴 안에서 단계가 일을 따라갑니다 — 그리고 읽기 도구 넷이 여태 줄을 서 있었습니다 |
|
|
1257
1259
|
| [1.17.4](docs/ko/releases/1.17.md#1174) | 같은 요청을 한 번 더 보내던 자리들 — 그리고 배운 것이 스스로를 봉인하던 자리 |
|
|
1258
1260
|
| [1.17.3](docs/ko/releases/1.17.md#1173) | 「같은 대화다」 를 적는 표준 이름이 없습니다 — 아는 이름을 다 답니다 |
|
|
1259
1261
|
| [1.17.2](docs/ko/releases/1.17.md#1172) | 다른 눈으로 한 번 더 — 한글이 못 여는 hwpx · 이름 없이 나가던 요청 둘 |
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/en/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -339,7 +339,7 @@ deel --offline
|
|
|
339
339
|
The destination is printed at the top of every session:
|
|
340
340
|
|
|
341
341
|
```
|
|
342
|
-
deel 1.17.
|
|
342
|
+
deel 1.17.6 ⌂ inside
|
|
343
343
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
344
344
|
```
|
|
345
345
|
|
|
@@ -1258,7 +1258,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
|
|
|
1258
1258
|
## Development
|
|
1259
1259
|
|
|
1260
1260
|
```bash
|
|
1261
|
-
npm test Full suite (
|
|
1261
|
+
npm test Full suite (8,011 checks; a few are TTY-dependent)
|
|
1262
1262
|
npm run coverage Which lines the tests actually execute
|
|
1263
1263
|
npm run verify Import + network checks only
|
|
1264
1264
|
npm run bench Edit success rate
|
|
@@ -1313,6 +1313,8 @@ so one run tells you everything.
|
|
|
1313
1313
|
|
|
1314
1314
|
| Version | What changed |
|
|
1315
1315
|
|---|---|
|
|
1316
|
+
| [1.17.6](docs/en/releases/1.17.md#1176) | Only keep-alive and no content used to mean waiting forever |
|
|
1317
|
+
| [1.17.5](docs/en/releases/1.17.md#1175) | The phase now follows the work inside a turn — and four read tools had been queueing up |
|
|
1316
1318
|
| [1.17.4](docs/en/releases/1.17.md#1174) | The places that sent the same request twice — and where what it learned sealed itself in |
|
|
1317
1319
|
| [1.17.3](docs/en/releases/1.17.md#1173) | No standard name for "same conversation" — it sends every name it knows |
|
|
1318
1320
|
| [1.17.2](docs/en/releases/1.17.md#1172) | A second pair of eyes — an hwpx Hancom cannot open · two requests going out unnamed |
|
package/package.json
CHANGED
package/src/acp/serve.js
CHANGED
|
@@ -50,7 +50,7 @@ import { allowEndpoint, setOffline } from '../safety/network.js';
|
|
|
50
50
|
import { 지금모드, 바깥인가, 나갈수있나 } from '../safety/runmode.js';
|
|
51
51
|
import { 주소가리기 } from '../safety/secrets.js';
|
|
52
52
|
import { probeCtx, 기본값 as CTX_DEFAULT } from '../backend/ctxsize.js';
|
|
53
|
-
import {
|
|
53
|
+
import { 잠잠기본, 무소식기본 } from '../backend/http.js';
|
|
54
54
|
import { 인증서설정 } from '../backend/clientcert.js';
|
|
55
55
|
import { 다붙이기 } from '../backend/mcp.js';
|
|
56
56
|
import { 배움 } from '../agent/evolve.js';
|
|
@@ -229,6 +229,8 @@ export async function acp(opts = {}) {
|
|
|
229
229
|
* 모았다가 한 번에 주는 사내 게이트웨이면 이 값을 올린다.
|
|
230
230
|
*/
|
|
231
231
|
잠잠: prof.잠잠 ?? prof.streamIdleMs ?? 잠잠기본,
|
|
232
|
+
// 바이트는 오는데 내용이 안 올 때의 전체 상한 (backend/http.js 의 무소식기본).
|
|
233
|
+
무소식: prof.무소식 ?? prof.streamNoNewsMs ?? 무소식기본,
|
|
232
234
|
// 게이트웨이가 우리 인증서를 요구하면 (mTLS, backend/clientcert.js).
|
|
233
235
|
// 파일 경로만 싣는다 — 알맹이는 요청 직전에 읽는다.
|
|
234
236
|
인증서: 인증서설정(prof),
|
package/src/agent/loop.js
CHANGED
|
@@ -20,6 +20,9 @@ import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
|
20
20
|
import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴까 } from '../safety/secrets.js';
|
|
21
21
|
import { 바깥인가 } from '../safety/runmode.js';
|
|
22
22
|
import { get as workMode } from './modes.js';
|
|
23
|
+
// 종합 모드에서 단계가 일을 따라간다 (agent/단계.js).
|
|
24
|
+
import { 다음단계 } from './단계.js';
|
|
25
|
+
import { 묻지말라했나, 손대라했나 } from './route.js';
|
|
23
26
|
import { 지시말 } from '../i18n/index.js';
|
|
24
27
|
import { 빠진것, 빠졌다는말 } from './asks.js';
|
|
25
28
|
import { 환경속열쇠들 } from '../config.js';
|
|
@@ -102,14 +105,31 @@ function thinkFor(conn, level) {
|
|
|
102
105
|
return level;
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
/*
|
|
109
|
+
* 아무것도 안 바꾸는 도구들. **이것들만** 동시에 돌린다.
|
|
110
|
+
*
|
|
111
|
+
* 왜 이것만인가:
|
|
112
|
+
* Read 세 개를 동시에 하는 것은 안전하다 — 서로 안 건드린다.
|
|
113
|
+
* Write·Edit 을 동시에 돌리면 같은 파일을 두 갈래로 고칠 수 있고,
|
|
114
|
+
* 되돌리기 스냅샷 순서도 뒤엉킨다. Bash 는 무슨 짓을 할지 알 수 없다.
|
|
115
|
+
* 그래서 '읽기만 하는 것' 이라고 확실한 도구만 묶는다.
|
|
116
|
+
*
|
|
117
|
+
* ── 그런데 이 목록이 modes.js 의 읽기 갈래보다 **짧았다** ───────────────
|
|
118
|
+
*
|
|
119
|
+
* modes.js 는 읽기만 하는 도구를 열 개로 세는데(Read·Outline·Glob·Grep·Def·
|
|
120
|
+
* Refs·WebFetch·Skill·Recall·Ask), 여기는 다섯 개만 알고 있었다. 나머지는
|
|
121
|
+
* 「안전한지 모르겠다」 가 아니라 그냥 **빠져 있었다.**
|
|
122
|
+
*
|
|
123
|
+
* 값이 큰 자리다. 시킴말이 남의 코드를 볼 때 **제일 먼저 부르라고 적어 둔
|
|
124
|
+
* 도구가 Outline** 이고(modes.js 의 읽는법), 그게 여태 한 개씩 돌았다.
|
|
125
|
+
* Def·Refs 는 언어 서버 왕복이라 기다리는 시간이 곧 사람이 기다리는 시간인데
|
|
126
|
+
* 그것도 한 줄로 섰다. 파일 여섯 개의 모양을 보는 데 여섯 번을 왕복했다.
|
|
127
|
+
*
|
|
128
|
+
* Ask 만 뺀다. 그건 사람에게 묻는 도구다 — 셋을 한꺼번에 물으면 화면에
|
|
129
|
+
* 물음 셋이 겹쳐 뜨고, 어느 답이 어느 물음의 것인지 사람이 못 가른다.
|
|
130
|
+
* 읽기 전용인 것은 맞지만 **사람의 차례를 쓰는 도구**라 줄을 세워야 한다.
|
|
131
|
+
*/
|
|
132
|
+
const 읽기전용 = new Set(['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'Recall', 'Skill', 'WebFetch']);
|
|
113
133
|
|
|
114
134
|
/*
|
|
115
135
|
* 잘린 답을 「생각을 줄여」 다시 부를 때 쓰는 두 값.
|
|
@@ -414,7 +434,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
414
434
|
* 여전히 설계 모드라고 떠 있는 채로 파일이 바뀐다. task.js 의 하위모드() 가
|
|
415
435
|
* 모드 쪽에서 한 겹 막고, 여기가 도구 쪽에서 한 겹 더 막는다.
|
|
416
436
|
*/
|
|
417
|
-
|
|
437
|
+
let tools = toolSchemas(session.도구제한 ?? null, {
|
|
418
438
|
hasSkills: (session.skills?.length ?? 0) > 0,
|
|
419
439
|
web: session.web !== false && !isOffline(), // 오프라인이면 웹 도구는 아예 안 보여 준다
|
|
420
440
|
work: session.effectiveWork(), // 작업 모드가 쓰는 것만 (modes.js)
|
|
@@ -440,17 +460,17 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
440
460
|
* 이름은 그 표에 없어서 undefined 를 읽다 죽는다. MCP 는 이름 목록이 아니라
|
|
441
461
|
* ctx.mcp 로 따로 넘어가므로 하위도 그 길로 똑같이 받는다.
|
|
442
462
|
*/
|
|
443
|
-
|
|
463
|
+
let 내도구 = tools.map((t) => t.function.name).filter((n) => !n.startsWith('mcp__'));
|
|
444
464
|
/*
|
|
445
465
|
* 깊이 상한. 부모(0) → 하위(1) → 하위의 하위(2) 까지다.
|
|
446
466
|
*
|
|
447
467
|
* 상한에 닿으면 목록에서 Task 를 뺀다. "더 쪼개지 마라" 고 부탁하지 않는다 —
|
|
448
468
|
* 모델은 부탁을 잊고, 잊으면 하위가 하위를 끝없이 낳는다.
|
|
449
469
|
*/
|
|
450
|
-
|
|
470
|
+
let 자식도구 = 깊이 + 1 >= 최대깊이 ? 내도구.filter((n) => n !== 'Task') : 내도구;
|
|
451
471
|
// 모드마다 생각의 배분과 걸음 수가 다르다. 사용자가 따로 정했으면 그걸 존중한다.
|
|
452
|
-
|
|
453
|
-
|
|
472
|
+
let 모드 = workMode(session.effectiveWork());
|
|
473
|
+
let effort = session.effortSet ? session.effort : (모드.effort ?? session.effort);
|
|
454
474
|
/*
|
|
455
475
|
* ── 강도는 **천장**이고, 이번 턴 값은 시킨 말이 정한다 (agent/effort.js) ──
|
|
456
476
|
*
|
|
@@ -472,8 +492,8 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
472
492
|
* 그래서 파일 열 개를 읽고 경합 조건을 짚어야 하는 일까지 low 로 돌았다 —
|
|
473
493
|
* 생각 블록이 0자로 나가고, 답이 그만큼 얕았다.
|
|
474
494
|
*/
|
|
475
|
-
|
|
476
|
-
|
|
495
|
+
let 천장 = session.thinkSet ? session.think : 천장고르기(모드.think, session.think, userText);
|
|
496
|
+
let think = 자동강도(userText, 천장, {
|
|
477
497
|
대화크기: session.messages.length,
|
|
478
498
|
켜짐: session.autoThink !== false,
|
|
479
499
|
});
|
|
@@ -492,7 +512,92 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
492
512
|
* 없다 — 8k 모델에는 너무 크고, 655k 모델은 여유가 96% 남았는데도 만들다
|
|
493
513
|
* 만 채로 끊겼다. 사람이 직접 준 값이 있으면 그것이 먼저다.
|
|
494
514
|
*/
|
|
495
|
-
|
|
515
|
+
let maxSteps = session.stepsSet ? session.maxSteps : 걸음수(모드.id, conn.ctx);
|
|
516
|
+
/*
|
|
517
|
+
* ── 단계가 일을 따라간다 (agent/단계.js) ────────────────────────────────
|
|
518
|
+
*
|
|
519
|
+
* 종합 모드는 한마디를 보고 단계를 고르는데, 그 고르기가 **턴이 시작할 때 딱
|
|
520
|
+
* 한 번** 돌았다. 마흔 걸음짜리 턴에서 일의 성격은 몇 번씩 바뀌는데, 시킴말은
|
|
521
|
+
* 첫 걸음의 것 그대로였다 — 원인을 다 찾고 고치는 중인데도 화면과 시킴말은
|
|
522
|
+
* 끝까지 「디버그」 였다.
|
|
523
|
+
*
|
|
524
|
+
* 여기서 세는 것은 **이미 일어난 일**이다. 모델에게 「지금 무슨 단계냐」 고
|
|
525
|
+
* 한 번 더 묻지 않는다. 그러면 걸음마다 왕복이 하나 늘고, 그 판단이 더
|
|
526
|
+
* 나을 까닭도 없다.
|
|
527
|
+
*/
|
|
528
|
+
const 이번턴본것 = { 할일: false, 바꿈: false };
|
|
529
|
+
const 묻지말라 = 묻지말라했나(userText);
|
|
530
|
+
// 계획에서 넘어갈 때만 본다. 「묻지 마라」 는 **어떻게**의 말이고,
|
|
531
|
+
// 「고쳐라」 는 **무엇을**의 말이다. 범위를 정하는 것은 뒤엣것이다(단계.js).
|
|
532
|
+
const 손대라 = 손대라했나(userText);
|
|
533
|
+
const 종합인가 = session.work === 'auto';
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 단계를 옮겨야 하면 옮기고, 옮겼으면 알린다.
|
|
537
|
+
*
|
|
538
|
+
* 시킴말은 저절로 따라온다 — session.wire() 가 effectiveWork() 를 매번 보고
|
|
539
|
+
* 짓기 때문이다(agent/session.js). 여기서 다시 지어야 하는 것은 **도구와
|
|
540
|
+
* 걸음 수와 생각의 배분**이다.
|
|
541
|
+
*
|
|
542
|
+
* @returns {object|null} 알릴 것이 있으면 이벤트, 없으면 null
|
|
543
|
+
*/
|
|
544
|
+
const 단계옮기기 = () => {
|
|
545
|
+
const 갈곳 = 다음단계({ 지금: 모드.id, 종합인가, 본것: 이번턴본것, 묻지말라, 손대라 });
|
|
546
|
+
if (!갈곳) return null;
|
|
547
|
+
const 옛모드 = 모드.id;
|
|
548
|
+
session.routed = 갈곳;
|
|
549
|
+
모드 = workMode(갈곳);
|
|
550
|
+
|
|
551
|
+
/*
|
|
552
|
+
* 도구가 늘면 **앞머리가 바뀐다.** 규격이 tools 를 맨 앞에 렌더하므로,
|
|
553
|
+
* 도구 하나가 늘어도 그 턴의 캐시는 거기서 끊긴다. 값이 드는 일이라
|
|
554
|
+
* 조용히 하지 않는다 — 화면에 적는다.
|
|
555
|
+
*/
|
|
556
|
+
const 옛도구 = new Set(내도구);
|
|
557
|
+
tools = toolSchemas(session.도구제한 ?? null, {
|
|
558
|
+
hasSkills: (session.skills?.length ?? 0) > 0,
|
|
559
|
+
web: session.web !== false && !isOffline(),
|
|
560
|
+
work: session.effectiveWork(),
|
|
561
|
+
mcp: ctx.mcp ?? null,
|
|
562
|
+
에이전트들: 깊이 + 1 >= 최대깊이 ? null : (ctx.에이전트들 ?? null),
|
|
563
|
+
lsp: session.lsp === true,
|
|
564
|
+
ctx: conn.ctx ?? null,
|
|
565
|
+
vision: conn.vision === true,
|
|
566
|
+
});
|
|
567
|
+
내도구 = tools.map((t) => t.function.name).filter((n) => !n.startsWith('mcp__'));
|
|
568
|
+
자식도구 = 깊이 + 1 >= 최대깊이 ? 내도구.filter((n) => n !== 'Task') : 내도구;
|
|
569
|
+
const 늘어난것 = 내도구.filter((n) => !옛도구.has(n));
|
|
570
|
+
|
|
571
|
+
// 생각의 배분도 그 단계 것으로. 사람이 직접 정했으면 그것이 먼저다.
|
|
572
|
+
if (!session.effortSet) effort = 모드.effort ?? session.effort;
|
|
573
|
+
if (!session.thinkSet) {
|
|
574
|
+
천장 = 천장고르기(모드.think, session.think, userText);
|
|
575
|
+
think = 자동강도(userText, 천장, {
|
|
576
|
+
대화크기: session.messages.length,
|
|
577
|
+
켜짐: session.autoThink !== false,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/*
|
|
581
|
+
* 걸음 수는 **늘리기만** 한다.
|
|
582
|
+
*
|
|
583
|
+
* 계획 모드의 상한은 코드 모드보다 작다. 넘어가면서 그 작은 값을 그대로
|
|
584
|
+
* 들고 가면 만들다 말고 끊긴다. 반대로 줄이면 이미 지난 걸음 수보다 작아져
|
|
585
|
+
* 그 자리에서 턴이 끝나 버릴 수도 있다 — 어느 쪽으로도 줄일 이유가 없다.
|
|
586
|
+
*/
|
|
587
|
+
if (!session.stepsSet) maxSteps = Math.max(maxSteps, 걸음수(모드.id, conn.ctx));
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
type: '단계옮김',
|
|
591
|
+
옛: 옛모드,
|
|
592
|
+
새: 갈곳,
|
|
593
|
+
// 화면 글이 아니라 **까닭 이름**을 낸다. 옮겨 적는 것은 화면의 몫이다
|
|
594
|
+
// (i18n/*.js 의 ev.phaseWhy*). 여기서 한국어를 내면 영어 화면에 섞인다.
|
|
595
|
+
왜: 이번턴본것.바꿈 ? '바꿈' : '할일',
|
|
596
|
+
늘어난도구: 늘어난것,
|
|
597
|
+
걸음: maxSteps,
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
496
601
|
const attempted = new Set(); // 같은 변경성 명령을 두 번 실행하지 않기 위한 기록
|
|
497
602
|
let steps = 0;
|
|
498
603
|
let lastToolFailed = false; // 직전 단계에서 도구가 오류를 냈나 → 다음 판단은 세게
|
|
@@ -688,6 +793,15 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
688
793
|
while (steps < maxSteps) {
|
|
689
794
|
steps++;
|
|
690
795
|
|
|
796
|
+
/*
|
|
797
|
+
* 단계를 **부르기 전에** 다시 본다 (위 단계옮기기).
|
|
798
|
+
*
|
|
799
|
+
* 부른 뒤에 옮기면 이번 걸음은 옛 시킴말로 나가고, 옮긴 보람이 한 걸음
|
|
800
|
+
* 늦는다. 마흔 걸음짜리 턴에서 그 한 걸음이 파일 내용 한 벌씩이다.
|
|
801
|
+
*/
|
|
802
|
+
const 옮김 = 단계옮기기();
|
|
803
|
+
if (옮김) yield 옮김;
|
|
804
|
+
|
|
691
805
|
// 부르기 **전에** 자리를 본다. 왜 여기인지는 자리만들기() 머리말에.
|
|
692
806
|
yield* 자리만들기();
|
|
693
807
|
if (접다멈췄나) return;
|
|
@@ -1817,6 +1931,18 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1817
1931
|
if (call.name === 'Read' && result.content) session.noteRead(call.args.file_path, result.content);
|
|
1818
1932
|
// 실제로 파일이 바뀐 것만 적는다. 턴 끝에 이 목록을 디스크와 견준다.
|
|
1819
1933
|
if (result.changed) 손댄파일.add(result.changed);
|
|
1934
|
+
/*
|
|
1935
|
+
* 단계도 이걸 본다 (agent/단계.js).
|
|
1936
|
+
*
|
|
1937
|
+
* 「바꾸기 시작했다」 는 조사가 끝났다는 뜻이다. 손댄파일 로 재도
|
|
1938
|
+
* 되지만 그 집합은 하위 작업이 바꾼 것까지 담으므로, 이 턴에서
|
|
1939
|
+
* **내가** 바꿨나를 따로 센다. 하위가 바꿨다고 부모의 단계가
|
|
1940
|
+
* 넘어가면, 부모는 아직 조사 중인데 시킴말만 바뀐다.
|
|
1941
|
+
*/
|
|
1942
|
+
if (result.changed || result.여럿?.some((f) => f.ok) || result.바뀐것들?.length) {
|
|
1943
|
+
이번턴본것.바꿈 = true;
|
|
1944
|
+
}
|
|
1945
|
+
if (call.name === 'TodoWrite' && !result.error) 이번턴본것.할일 = true;
|
|
1820
1946
|
// 한 번에 여러 개를 쓴 경우. changed 하나만 보면 나머지가 조용히 빠져서,
|
|
1821
1947
|
// 턴 끝에 "만들어졌다" 고 확인해 주는 파일이 넷 중 하나만 나온다.
|
|
1822
1948
|
for (const f of result.여럿 ?? []) if (f.ok && f.path) 손댄파일.add(f.path);
|
package/src/agent/models.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { load, activeProfile, resolveKey } from '../config.js';
|
|
36
36
|
import { isLocalHost, isOffline } from '../safety/network.js';
|
|
37
|
-
import {
|
|
37
|
+
import { 잠잠기본, 무소식기본 } from '../backend/http.js';
|
|
38
38
|
import { 인증서설정 } from '../backend/clientcert.js';
|
|
39
39
|
|
|
40
40
|
// 컨텍스트를 못 알아냈을 때 쓰는 값. repl.js 와 같은 값을 봐야 한다.
|
|
@@ -59,6 +59,8 @@ export function 연결만들기(prof, { ctx = null, maxTokens = null } = {}) {
|
|
|
59
59
|
maxTokens: maxTokens ?? prof.maxTokens ?? null,
|
|
60
60
|
streaming: prof.streaming ?? false,
|
|
61
61
|
잠잠: prof.잠잠 ?? prof.streamIdleMs ?? 잠잠기본,
|
|
62
|
+
// 바이트는 오는데 내용이 안 올 때의 전체 상한 (backend/http.js 의 무소식기본).
|
|
63
|
+
무소식: prof.무소식 ?? prof.streamNoNewsMs ?? 무소식기본,
|
|
62
64
|
인증서: 인증서설정(prof),
|
|
63
65
|
tools: prof.tools ?? false,
|
|
64
66
|
json: prof.json ?? false,
|
package/src/agent/modes.js
CHANGED
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
// Ask 는 **모든 모드**에 있다(읽기 갈래에 둔 이유가 그것이다). 갈림길은
|
|
19
19
|
// 어느 모드에서나 생기고, 물어볼 길이 없으면 모델은 글로 "알려주세요" 하고
|
|
20
20
|
// 턴을 끝내 버린다 — 그러면 여태 조사한 것이 통째로 버려진다.
|
|
21
|
-
|
|
21
|
+
// 내보낸다 — loop.js 의 동시에 돌릴 목록이 이것보다 짧아지면 검사가 잡게(test/parallel.test.js).
|
|
22
|
+
// 두 곳에 같은 목록을 따로 적어 둔 탓에 다섯 개가 몇 달 동안 줄을 서서 돌았다.
|
|
23
|
+
export const 읽기도구 = ['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'WebFetch', 'Skill', 'Recall', 'Ask'];
|
|
24
|
+
const 읽기 = 읽기도구;
|
|
22
25
|
// 계획을 적는 도구. 파일을 안 건드리므로 읽기 전용 모드에서도 준다.
|
|
23
26
|
//
|
|
24
27
|
// Remember 도 여기 있다. 기억은 사용자의 소스를 안 건드리고 .deel/memory.md
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 종합 모드에서 **단계가 일을 따라간다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 문제였나 ─────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 종합 모드는 한마디를 보고 알맞은 작업 모드를 고른다(agent/route.js). 그런데
|
|
7
|
+
* 그 고르기가 **턴이 시작할 때 딱 한 번** 돌았다. 고른 뒤로는 그 턴이 끝날
|
|
8
|
+
* 때까지 안 바뀐다.
|
|
9
|
+
*
|
|
10
|
+
* 짧은 한마디에는 맞는 방식인데, 긴 일에서는 그게 곧 고장이다. 한 턴이 마흔
|
|
11
|
+
* 걸음을 도는 동안 일의 성격은 몇 번씩 바뀐다 —
|
|
12
|
+
*
|
|
13
|
+
* 「왜 안 되는지 찾아서 고쳐줘」 → 디버그로 시작한다. 열두 걸음쯤에 원인을
|
|
14
|
+
* 찾는다. 그때부터 남은 스물여덟 걸음은 **고치는 일**인데, 시킴말은 여전히
|
|
15
|
+
* 「원인을 찾아라 · 고치기 전에 재현부터 하라」 고 말하고 있다.
|
|
16
|
+
*
|
|
17
|
+
* 화면에도 `~디버그` 가 끝까지 떠 있다. 사람이 「알아서 계획·디버그·코드로
|
|
18
|
+
* 넘어가 달라」 고 한 것이 이 자리다.
|
|
19
|
+
*
|
|
20
|
+
* ── 무엇으로 옮기나 ─────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* 모델에게 「지금 무슨 단계냐」 고 한 번 더 묻지 않는다. 그러면 걸음마다 왕복이
|
|
23
|
+
* 하나 늘고, 그 판단은 route.js 가 낱말로 하던 것보다 나을 것도 없다.
|
|
24
|
+
*
|
|
25
|
+
* **이미 일어난 일**로 옮긴다. 짐작이 아니라 잰 것이다.
|
|
26
|
+
*
|
|
27
|
+
* 할 일을 적었다 (TodoWrite 에 남은 일이 있다) → 계획은 끝났다
|
|
28
|
+
* 파일을 실제로 바꿨다 → 만드는 중이다
|
|
29
|
+
*
|
|
30
|
+
* ── 앞으로만 간다 ───────────────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* 되돌아가지 않는다. 한 걸음 읽었다고 다시 조사 단계로 내려가면 단계가
|
|
33
|
+
* 걸음마다 오르내리고, 그때마다 시킴말이 바뀐다 — 그건 캐시를 걸음마다
|
|
34
|
+
* 깨뜨리면서 모델에게는 갈피를 못 잡게 하는 짓이다.
|
|
35
|
+
*
|
|
36
|
+
* ── 사람이 고른 모드는 안 건드린다 ──────────────────────────────────────
|
|
37
|
+
*
|
|
38
|
+
* `/plan` 을 직접 친 사람에게 계획 모드는 **약속**이다. 여기서 옮기는 것은
|
|
39
|
+
* 라우터가 **짐작한** 단계뿐이다. 그 둘을 가르는 것은 `종합인가` 하나다.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* ── 단계가 넘어가는 표 ──────────────────────────────────────────────────
|
|
44
|
+
*
|
|
45
|
+
* 왼쪽에서 오른쪽으로만 간다. 여기 없는 모드는 안 옮긴다.
|
|
46
|
+
*
|
|
47
|
+
* auto → code 무슨 일인지 못 고른 채로 시작했는데 파일을 바꾸기 시작했다.
|
|
48
|
+
* 그러면 지금 하는 일은 만드는 일이다.
|
|
49
|
+
* debug → code 원인을 찾아 고치기 시작했다. 남은 걸음은 만드는 일이다.
|
|
50
|
+
* plan → code 계획을 다 적었다. 다만 이건 조건이 하나 더 붙는다 (아래).
|
|
51
|
+
*
|
|
52
|
+
* `architect` · `inspect` · `ask` 는 **안 옮긴다.**
|
|
53
|
+
*
|
|
54
|
+
* 셋 다 라우터가 「이 턴은 말로 답하는 턴」 이라고 본 자리다. 그리고 그렇게
|
|
55
|
+
* 보려면 사람의 말에 고치라는 말이 없어야 한다(route.js 의 손대라했나 가
|
|
56
|
+
* 그 셋을 후보에서 뺀다). 즉 **사람이 고쳐 달라고 안 했다.** 안 시킨 일을
|
|
57
|
+
* 스스로 시작하는 것은 단계가 나아가는 것이 아니라 시킨 범위를 넘는 것이다.
|
|
58
|
+
*/
|
|
59
|
+
const 넘어갈곳 = {
|
|
60
|
+
auto: 'code',
|
|
61
|
+
debug: 'code',
|
|
62
|
+
plan: 'code',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* `plan` 만 조건이 더 붙는다. **두 개다.**
|
|
67
|
+
*
|
|
68
|
+
* 계획 모드는 파일을 바꾸는 도구가 없다. 그래서 여기서 넘어가면 도구가
|
|
69
|
+
* 늘어나고, 늘어난 도구는 앞머리를 바꾸므로 캐시가 한 번 끊긴다. 값이 드는
|
|
70
|
+
* 일이라 아무 때나 하면 안 된다. 그보다 더 큰 것은, 이 자리가 **시킨 범위를
|
|
71
|
+
* 넘을 수 있는 유일한 자리**라는 점이다 — 다른 단계는 이미 파일을 바꿀 수
|
|
72
|
+
* 있는 자리에서 시작하지만 계획은 아니다.
|
|
73
|
+
*
|
|
74
|
+
* (1) 사람이 「묻지 말고 끝까지」 라고 적어 뒀어야 한다.
|
|
75
|
+
*
|
|
76
|
+
* 「계획해서 만들어줘」 는 이미 다른 길이 있다 — 계획을 내고 승인을
|
|
77
|
+
* 받아 이어서 하는 길이다(route.js 의 겹친요청). 그 길이 있는데 여기서
|
|
78
|
+
* 몰래 넘어가면 사람이 보자고 한 계획을 못 보고 파일부터 바뀐다.
|
|
79
|
+
* 그런데 겹침 길은 「묻지 말라」 고 적은 사람에게는 꺼져 있다. 그 사람
|
|
80
|
+
* 에게는 승인 창이 값이 아니라 손해이기 때문이다. 그러면 계획만 내고
|
|
81
|
+
* 멈추는 턴이 남는다 — 시킨 일의 절반이다. 그 나머지 절반이 이 자리다.
|
|
82
|
+
*
|
|
83
|
+
* (2) 그리고 사람이 **고쳐 달라고 했어야 한다** (route.js 의 손대라했나).
|
|
84
|
+
*
|
|
85
|
+
* (1)만으로는 구멍이 하나 남는다. 「계획만 세워줘. 알아서 진행해」 처럼
|
|
86
|
+
* **계획만 원하면서 묻지 말라고도 적은** 말이 있다. 그 사람은 승인 창이
|
|
87
|
+
* 싫다고 했을 뿐 파일을 고쳐 달라고 한 적이 없다. 그런데 (1)만 보면
|
|
88
|
+
* 할 일을 적는 순간 코드로 넘어가 파일이 바뀐다.
|
|
89
|
+
*
|
|
90
|
+
* 두 말은 뜻이 다르다 — 「묻지 마라」 는 **어떻게** 하라는 말이고,
|
|
91
|
+
* 「고쳐라」 는 **무엇을** 하라는 말이다. 범위를 정하는 것은 뒤엣것이다.
|
|
92
|
+
* 그래서 둘 다 있어야 넘어간다.
|
|
93
|
+
*/
|
|
94
|
+
const 말에있어야하는것 = {
|
|
95
|
+
plan: (말) => 말.묻지말라 === true && 말.손대라 === true,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** 이 단계에서 다음으로 넘어가려면 무엇이 일어나야 하나. */
|
|
99
|
+
const 신호 = {
|
|
100
|
+
// 파일을 바꾸기 전에는 아직 조사 중이다.
|
|
101
|
+
auto: (본것) => 본것?.바꿈 === true,
|
|
102
|
+
debug: (본것) => 본것?.바꿈 === true,
|
|
103
|
+
// 계획은 파일을 못 바꾼다. 그러니 「바꿨나」 로는 영영 안 넘어간다.
|
|
104
|
+
// 계획이 끝났다는 신호는 **적어 둔 할 일**이다.
|
|
105
|
+
plan: (본것) => 본것?.할일 === true,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 지금 걸음에서 어느 단계여야 하나.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} 것
|
|
112
|
+
* @param {string} 것.지금 지금 단계 (작업 모드 id)
|
|
113
|
+
* @param {boolean} 것.종합인가 사람이 고른 모드가 아니라 종합 모드인가
|
|
114
|
+
* @param {object} 것.본것 { 할일: boolean, 바꿈: boolean } — 이번 턴에 일어난 일
|
|
115
|
+
* @param {boolean} 것.묻지말라 사람이 「묻지 말고 끝까지」 라고 적어 뒀나
|
|
116
|
+
* @param {boolean} 것.손대라 사람이 고쳐 달라고 했나 (route.js 의 손대라했나)
|
|
117
|
+
* @returns {string|null} 옮길 곳. 안 옮기면 null.
|
|
118
|
+
*/
|
|
119
|
+
export function 다음단계({ 지금, 종합인가, 본것, 묻지말라 = false, 손대라 = false }) {
|
|
120
|
+
// 사람이 고른 모드는 약속이다. 짐작만 옮긴다.
|
|
121
|
+
if (!종합인가) return null;
|
|
122
|
+
const 갈곳 = 넘어갈곳[지금];
|
|
123
|
+
if (!갈곳 || 갈곳 === 지금) return null;
|
|
124
|
+
const 잠금 = 말에있어야하는것[지금];
|
|
125
|
+
if (잠금 && !잠금({ 묻지말라, 손대라 })) return null;
|
|
126
|
+
return 신호[지금]?.(본것) ? 갈곳 : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 넘어가면서 도구가 늘어나나 — 늘면 앞머리를 다시 쓰게 되므로 사람에게 말해야 한다. */
|
|
130
|
+
export function 도구가늘어나나(옛모드, 새모드, 도구가져오기) {
|
|
131
|
+
const 옛 = new Set(도구가져오기(옛모드));
|
|
132
|
+
return 도구가져오기(새모드).some((t) => !옛.has(t));
|
|
133
|
+
}
|
package/src/backend/adapter.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// 진단(probe)과 에이전트 루프가 같은 함수를 쓴다.
|
|
3
3
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
req, headersFor, serverMessage, Aborted, 잠잠기본, 초로,
|
|
7
|
+
무소식기본, 무소식알림기본, 소식없음오류,
|
|
8
|
+
} from './http.js';
|
|
6
9
|
import { 할당량기억, 미리기다릴까, 마지막할당량, 할당량자리 } from './quota.js';
|
|
7
10
|
import { 열쇠 as 열쇠받아오기, 쓸수있나 } from '../safety/authcmd.js';
|
|
8
11
|
import { 말 } from '../i18n/index.js';
|
|
@@ -1223,6 +1226,32 @@ export async function* chatStream(conn, opts) {
|
|
|
1223
1226
|
const dec = new TextDecoder();
|
|
1224
1227
|
let buf = '';
|
|
1225
1228
|
|
|
1229
|
+
/*
|
|
1230
|
+
* ── 바이트 말고 **소식**을 잰다 (backend/http.js 의 무소식기본) ─────────
|
|
1231
|
+
*
|
|
1232
|
+
* 잠잠 시계는 조각이 오면 되감긴다. 그런데 keep-alive(`: ping`)와 빈 delta
|
|
1233
|
+
* 는 조각이면서 내용이 없다 — 되감기기만 하고 화면에는 아무것도 안 남는다.
|
|
1234
|
+
* 그 둘만 오는 동안은 천장이 사라진다.
|
|
1235
|
+
*
|
|
1236
|
+
* 여기서 세는 것은 **acc 가 자랐나** 다. 이벤트 수가 아니다 — 도구 인자는
|
|
1237
|
+
* 글자로 쪼개져 와서 acc._raw 에만 쌓이고 이벤트를 안 낸다(mergeDeltaCalls).
|
|
1238
|
+
* 이벤트만 세면 도구 호출을 길게 쓰는 중인 정상 흐름을 우리가 끊는다.
|
|
1239
|
+
*/
|
|
1240
|
+
const 자란만큼 = () =>
|
|
1241
|
+
acc.content.length
|
|
1242
|
+
+ acc.thinking.length
|
|
1243
|
+
+ (acc.거절글?.length ?? 0)
|
|
1244
|
+
+ (acc._raw?.reduce(
|
|
1245
|
+
(n, c) => n + 1 + (c?.name?.length ?? 0) + (c?.args?.length ?? 0), 0) ?? 0)
|
|
1246
|
+
+ (acc.usage?.잰것 ? 1 : 0)
|
|
1247
|
+
+ (acc.stopped ? 1 : 0);
|
|
1248
|
+
|
|
1249
|
+
const 무소식 = conn.무소식 ?? 무소식기본;
|
|
1250
|
+
const 무소식알림 = Math.min(conn.무소식알림 ?? 무소식알림기본, 무소식 > 0 ? 무소식 / 2 : Infinity);
|
|
1251
|
+
let 마지막소식 = Date.now();
|
|
1252
|
+
let 본만큼 = 자란만큼();
|
|
1253
|
+
let 알렸나 = false;
|
|
1254
|
+
|
|
1226
1255
|
/*
|
|
1227
1256
|
* 한 줄을 읽는 규칙. 루프 안에서도, **끝난 뒤에도** 같은 것을 쓴다.
|
|
1228
1257
|
*
|
|
@@ -1292,6 +1321,51 @@ export async function* chatStream(conn, opts) {
|
|
|
1292
1321
|
yield* 한줄(꼬리);
|
|
1293
1322
|
break;
|
|
1294
1323
|
}
|
|
1324
|
+
|
|
1325
|
+
/*
|
|
1326
|
+
* 이번 조각으로 acc 가 자랐나. 자랐으면 시계를 되감고, 안 자랐으면 얼마나
|
|
1327
|
+
* 오래 그랬는지 본다.
|
|
1328
|
+
*
|
|
1329
|
+
* 알림이 먼저고 끊기는 나중이다. 순서가 중요하다 — 끊기부터 만들면
|
|
1330
|
+
* 「원래 이런 게이트웨이」 를 우리가 죽이면서 그 사실을 아무도 못 본다.
|
|
1331
|
+
*/
|
|
1332
|
+
const 이번만큼 = 자란만큼();
|
|
1333
|
+
if (이번만큼 !== 본만큼) {
|
|
1334
|
+
본만큼 = 이번만큼;
|
|
1335
|
+
마지막소식 = Date.now();
|
|
1336
|
+
알렸나 = false;
|
|
1337
|
+
} else {
|
|
1338
|
+
const 흐른 = Date.now() - 마지막소식;
|
|
1339
|
+
if (무소식 > 0 && 흐른 >= 무소식) {
|
|
1340
|
+
/*
|
|
1341
|
+
* 끊을 때는 **소켓도 같이 닫는다.**
|
|
1342
|
+
*
|
|
1343
|
+
* 안 닫으면 게이트웨이는 우리가 간 줄도 모르고 핑을 계속 보낸다. 그
|
|
1344
|
+
* 연결이 프로그램이 끝날 때까지 남아서 겪을 때마다 하나씩 쌓인다.
|
|
1345
|
+
* 잠잠 시계는 제 손으로 소켓을 끊고 있다(http.js 의 끊기) — 여기만
|
|
1346
|
+
* 안 하면 같은 일을 반쪽만 하는 것이다. 검사에서 프로세스가 안 끝나서
|
|
1347
|
+
* 잡혔다.
|
|
1348
|
+
*/
|
|
1349
|
+
try { await reader.cancel(); } catch { /* 이미 닫혔으면 그만 */ }
|
|
1350
|
+
/*
|
|
1351
|
+
* 여기까지 받은 글은 **버리지 않는다.** 세 문단을 받아 놓고 그 뒤로
|
|
1352
|
+
* keep-alive 만 오는 자리가 있다 — 통째로 던지면 사람은 아무것도 못
|
|
1353
|
+
* 보고 같은 것을 다시 시킨다. 잠잠 시계가 하는 것과 같은 규칙이다.
|
|
1354
|
+
*
|
|
1355
|
+
* 한 글자도 못 받았으면 그건 그냥 실패다. 빈 답을 답이라고 부르지 않는다.
|
|
1356
|
+
*/
|
|
1357
|
+
if (acc.content || acc.thinking || acc.toolCalls.length) {
|
|
1358
|
+
acc.stopped = 흐름멎음;
|
|
1359
|
+
acc.멎은초 = 초로(흐른);
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
throw 소식없음오류(무소식);
|
|
1363
|
+
}
|
|
1364
|
+
if (!알렸나 && 흐른 >= 무소식알림) {
|
|
1365
|
+
알렸나 = true;
|
|
1366
|
+
yield { type: '소식없음', 초: 초로(흐른), 상한초: 무소식 > 0 ? 초로(무소식) : null };
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1295
1369
|
}
|
|
1296
1370
|
/*
|
|
1297
1371
|
* ── 끝을 안 알려 주고 끊긴 것은 '끝난 것' 이 아니다 ──────────────────────
|
package/src/backend/http.js
CHANGED
|
@@ -232,6 +232,57 @@ export function 멎음오류(잠잠) {
|
|
|
232
232
|
);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/*
|
|
236
|
+
* ── 위 시계가 못 잡는 자리 ──────────────────────────────────────────────
|
|
237
|
+
*
|
|
238
|
+
* 잠잠 시계는 **바이트**를 잰다. 조각이 하나 오면 되감긴다(위 잠잠감시).
|
|
239
|
+
* 그런데 SSE 에는 내용이 없는 조각이 있다 —
|
|
240
|
+
*
|
|
241
|
+
* `: ping` 규격이 정한 주석 줄. keep-alive.
|
|
242
|
+
* `data: {"choices":[{"delta":{}}]}` 빈 delta. 살아 있다는 말만 한다.
|
|
243
|
+
*
|
|
244
|
+
* 둘 다 **바이트**다. 그래서 잠잠 시계를 되감는다. 그리고 둘 다 화면에
|
|
245
|
+
* 아무것도 안 남긴다 — 앞엣것은 JSON 이 아니라 파싱에서 버려지고, 뒤엣것은
|
|
246
|
+
* absorb 가 낼 이벤트가 없다.
|
|
247
|
+
*
|
|
248
|
+
* 머리말까지 재던 시계(timeout)는 머리말이 오는 순간 **꺼진다**(위 노드로·
|
|
249
|
+
* 직접흘려의 시계끄기). 그러면 남는 천장이 하나도 없다.
|
|
250
|
+
*
|
|
251
|
+
* 실제로 그렇게 됐다. 게이트웨이 뒤에서 15분 넘게, 오류도 없이, 스피너만
|
|
252
|
+
* 돌았다. 이벤트 루프는 멀쩡했다 — 기다릴 이유가 없어지는 순간이 영영 안
|
|
253
|
+
* 왔을 뿐이다.
|
|
254
|
+
*
|
|
255
|
+
* 그래서 시계를 하나 더 둔다. 바이트가 아니라 **소식**을 잰다. 흔히
|
|
256
|
+
* Logical Idle Timeout 이라 부르고, 그 위에 전체 상한(Total Timeout)을
|
|
257
|
+
* 같이 씌우는 것이 보통이다. 여기서는 둘을 한 쌍으로 둔다.
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 소식 없이 얼마나 두고 볼까 — **알리는** 자리.
|
|
262
|
+
*
|
|
263
|
+
* 끊지 않는다. 한 줄 적을 뿐이다. 추론을 감추는 게이트웨이는 원래 이 모양으로
|
|
264
|
+
* 정상 동작한다 — 상위 모델이 생각하는 동안 keep-alive 만 흘린다. 그걸
|
|
265
|
+
* 끊어 버리면 지금 잘 도는 연결을 우리가 죽이는 것이다.
|
|
266
|
+
*
|
|
267
|
+
* 90초로 잡는다. 사람이 「멈춘 건가」 를 의심하기 시작하는 자리다.
|
|
268
|
+
*/
|
|
269
|
+
export const 무소식알림기본 = 90000;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 그래도 이건 넘기지 않는다 — **전체 상한**.
|
|
273
|
+
*
|
|
274
|
+
* 무한은 어떤 경우에도 맞는 답이 아니다. 10분은 어떤 모델의 첫 글자보다도
|
|
275
|
+
* 한참 뒤다. 0 으로 두면 이 상한을 안 쓴다(프로필의 `무소식`).
|
|
276
|
+
*/
|
|
277
|
+
export const 무소식기본 = 600000;
|
|
278
|
+
|
|
279
|
+
export function 소식없음오류(무소식) {
|
|
280
|
+
return Object.assign(
|
|
281
|
+
new Error(`${초로(무소식)}초 동안 살아 있다는 신호만 오고 내용이 안 왔습니다`),
|
|
282
|
+
{ name: 'NoNewsError', code: 'NONEWS', 무소식 },
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
235
286
|
/**
|
|
236
287
|
* 밀리초를 사람에게 보여 줄 초로.
|
|
237
288
|
*
|
package/src/commands.js
CHANGED
|
@@ -30,7 +30,7 @@ import { 말, 말 as 옮긴말, 언어, 언어들, 언어정하기, 언어고르
|
|
|
30
30
|
import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './agent/models.js';
|
|
31
31
|
import { allowTemporarily } from './safety/network.js';
|
|
32
32
|
import { chat, 규격이름, 더할머리 } from './backend/adapter.js';
|
|
33
|
-
import {
|
|
33
|
+
import { 잠잠기본, 무소식기본 } from './backend/http.js';
|
|
34
34
|
import { 인증서설정, 인증서말 } from './backend/clientcert.js';
|
|
35
35
|
import { 알림채움 } from './backend/retry.js';
|
|
36
36
|
import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
|
|
@@ -2944,6 +2944,8 @@ function 연결적용(session, p, ctx = null) {
|
|
|
2944
2944
|
streaming: p.streaming, tools: p.tools, json: p.json, think: p.think,
|
|
2945
2945
|
// 프로필마다 다를 수 있다 — 사내 게이트웨이는 잠잠 상한이 크고 로컬은 작다.
|
|
2946
2946
|
잠잠: p.잠잠 ?? p.streamIdleMs ?? 잠잠기본,
|
|
2947
|
+
// 바이트는 오는데 내용이 안 올 때의 전체 상한 (backend/http.js 의 무소식기본).
|
|
2948
|
+
무소식: p.무소식 ?? p.streamNoNewsMs ?? 무소식기본,
|
|
2947
2949
|
// 프로필마다 인증서가 다르다. 안 갈아 끼우면 옛 프로필의 신원으로 붙는다.
|
|
2948
2950
|
인증서: 인증서설정(p),
|
|
2949
2951
|
/*
|
package/src/i18n/en.js
CHANGED
|
@@ -474,6 +474,12 @@ export const en = {
|
|
|
474
474
|
'run.stopping': 'stopping…',
|
|
475
475
|
'run.errKept': 'What arrived before the failure was kept in the conversation — you can carry on from there.',
|
|
476
476
|
'ev.capped': 'The answer was cut off at {한계} tokens — raise the cap with /out, or have files written in parts',
|
|
477
|
+
'ev.phaseMove': '{왜} — from here on this runs as {새}, not {옛}',
|
|
478
|
+
'ev.phaseWhyEdit': 'Started changing files',
|
|
479
|
+
'ev.phaseWhyTodo': 'The plan is written down',
|
|
480
|
+
'ev.phaseTools': 'More tools from here ({도구}) — the prefix gets written once more',
|
|
481
|
+
'ev.noNews': 'Only keep-alive for {초}s, no content yet — the gateway may still be waiting on the upstream model',
|
|
482
|
+
'ev.noNewsCap': 'Giving up at {상한초}s. If this is genuinely slow, raise streamNoNewsMs in the profile',
|
|
477
483
|
'ev.cutoff': 'The server stopped the answer without saying it had finished — it may have been cut mid-way',
|
|
478
484
|
'ev.stalled': 'Nothing arrived for {n}s, so the connection was dropped — this is what came through',
|
|
479
485
|
'ev.nudgeMissed': 'It was about to finish leaving {n} of the things you asked for undone — pushed back once',
|
package/src/i18n/ja.js
CHANGED
|
@@ -432,6 +432,12 @@ export const ja = {
|
|
|
432
432
|
'run.stopping': '止めています…',
|
|
433
433
|
'run.errKept': 'ここまで届いた分は会話に残しました — このまま続けて話せます。',
|
|
434
434
|
'ev.capped': '答えが {한계} トークンで切れました — /out で上限を上げるか、ファイルは分けて書かせてください',
|
|
435
|
+
'ev.phaseMove': '{왜} — ここからは {옛} ではなく {새} で進めます',
|
|
436
|
+
'ev.phaseWhyEdit': 'ファイルを変更し始めました',
|
|
437
|
+
'ev.phaseWhyTodo': 'やることを書き終えました',
|
|
438
|
+
'ev.phaseTools': 'ツールが増えました ({도구}) — 先頭を一度書き直します',
|
|
439
|
+
'ev.noNews': '{초}秒間生存信号だけで内容が来ていません — ゲートウェイが上位モデルを待っている可能性があります',
|
|
440
|
+
'ev.noNewsCap': '{상한초}秒で打ち切ります。本当に遅い場合は設定の streamNoNewsMs を上げてください',
|
|
435
441
|
'ev.cutoff': 'サーバーが終わったとも言わずに答えを止めました — 途中で切れた可能性があります',
|
|
436
442
|
'ev.stalled': '{n}秒間なにも届かなかったので切りました — 届いたぶんだけ表示します',
|
|
437
443
|
'ev.nudgeMissed': '頼んだこと {n} 件を残したまま終えようとしたので、一度押し戻しました',
|
package/src/i18n/ko.js
CHANGED
|
@@ -505,6 +505,12 @@ export const ko = {
|
|
|
505
505
|
* `deel -p` 뒤 스크립트로 넘어가던 것이 그 탓이다.
|
|
506
506
|
*/
|
|
507
507
|
'ev.capped': '답이 {한계} 토큰에서 잘렸습니다 — /out 으로 상한을 올리거나, 파일은 나눠 쓰게 하세요',
|
|
508
|
+
'ev.phaseMove': '{왜} — 여기서부터 {옛} 대신 {새} 로 갑니다',
|
|
509
|
+
'ev.phaseWhyEdit': '파일을 바꾸기 시작했습니다',
|
|
510
|
+
'ev.phaseWhyTodo': '할 일을 다 적었습니다',
|
|
511
|
+
'ev.phaseTools': '도구가 늘었습니다 ({도구}) — 앞머리를 한 번 다시 씁니다',
|
|
512
|
+
'ev.noNews': '{초}초간 살아 있다는 신호만 오고 내용은 안 왔습니다 — 게이트웨이가 윗모델을 기다리는 중일 수 있습니다',
|
|
513
|
+
'ev.noNewsCap': '{상한초}초까지 기다리고 끊습니다. 오래 걸리는 게 맞으면 설정의 무소식 을 올리세요',
|
|
508
514
|
'ev.cutoff': '서버가 끝났다는 말 없이 답을 멈췄습니다 — 중간에서 끊겼을 수 있습니다',
|
|
509
515
|
'ev.stalled': '{n}초 동안 아무것도 안 와서 끊었습니다 — 받은 데까지만 보여 드립니다',
|
|
510
516
|
'ev.nudgeMissed': '시킨 것 {n}가지를 빼놓고 끝내려고 해서 한 번 되밀었습니다',
|
package/src/i18n/zh.js
CHANGED
|
@@ -432,6 +432,12 @@ export const zh = {
|
|
|
432
432
|
'run.stopping': '正在停止…',
|
|
433
433
|
'run.errKept': '出错前收到的内容已经留在对话里 — 可以直接接着说。',
|
|
434
434
|
'ev.capped': '回答在 {한계} 个 token 处被截断 — 用 /out 调高上限,或者让文件分次写',
|
|
435
|
+
'ev.phaseMove': '{왜} — 从这里开始按 {새} 而不是 {옛} 进行',
|
|
436
|
+
'ev.phaseWhyEdit': '开始修改文件了',
|
|
437
|
+
'ev.phaseWhyTodo': '要做的事已经列完了',
|
|
438
|
+
'ev.phaseTools': '工具变多了 ({도구}) — 前缀要重写一次',
|
|
439
|
+
'ev.noNews': '{초} 秒内只有保活信号,没有内容 — 网关可能还在等上游模型',
|
|
440
|
+
'ev.noNewsCap': '到 {상한초} 秒就断开。如果确实慢,请调高配置中的 streamNoNewsMs',
|
|
435
441
|
'ev.cutoff': '服务端没说结束就停下了 — 回答可能是中途断的',
|
|
436
442
|
'ev.stalled': '{n} 秒没有任何数据,已断开 — 只显示已收到的部分',
|
|
437
443
|
'ev.nudgeMissed': '还剩你交代的 {n} 件事没做就要收尾,已经推回去一次',
|
package/src/oneshot.js
CHANGED
|
@@ -35,10 +35,10 @@ import { allowEndpoint, setOffline } from './safety/network.js';
|
|
|
35
35
|
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
36
36
|
import { 주소가리기 } from './safety/secrets.js';
|
|
37
37
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
38
|
-
import {
|
|
38
|
+
import { 잠잠기본, 무소식기본 } from './backend/http.js';
|
|
39
39
|
import { 인증서설정 } from './backend/clientcert.js';
|
|
40
40
|
import { route } from './agent/route.js';
|
|
41
|
-
import { get as getWork } from './agent/modes.js';
|
|
41
|
+
import { get as getWork, 보일이름 } from './agent/modes.js';
|
|
42
42
|
import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/jobs.js';
|
|
43
43
|
import { 첫이름 } from './tools/label.js';
|
|
44
44
|
|
|
@@ -274,6 +274,8 @@ export async function runOnce(opts = {}) {
|
|
|
274
274
|
* 모았다가 한 번에 주는 사내 게이트웨이면 이 값을 올린다.
|
|
275
275
|
*/
|
|
276
276
|
잠잠: prof.잠잠 ?? prof.streamIdleMs ?? 잠잠기본,
|
|
277
|
+
// 바이트는 오는데 내용이 안 올 때의 전체 상한 (backend/http.js 의 무소식기본).
|
|
278
|
+
무소식: prof.무소식 ?? prof.streamNoNewsMs ?? 무소식기본,
|
|
277
279
|
// 게이트웨이가 우리 인증서를 요구하면 (mTLS, backend/clientcert.js).
|
|
278
280
|
// 파일 경로만 싣는다 — 알맹이는 요청 직전에 읽는다.
|
|
279
281
|
인증서: 인증서설정(prof),
|
|
@@ -612,6 +614,27 @@ export async function runOnce(opts = {}) {
|
|
|
612
614
|
곁(` ${c.cyan('⚙')} ${c.gray(옮긴말('loop.wire', { 무엇: ev.무엇 }))}`);
|
|
613
615
|
break;
|
|
614
616
|
|
|
617
|
+
/*
|
|
618
|
+
* 종합 모드가 턴 도중에 단계를 옮겼다 (agent/단계.js).
|
|
619
|
+
*
|
|
620
|
+
* 한 방 실행에서 특히 남겨야 하는 줄이다. `deel -p` 는 잡·CI 에서 돌고
|
|
621
|
+
* 그 기록만 나중에 남는다 — 「계획만 내고 끝날 줄 알았는데 파일이
|
|
622
|
+
* 바뀌어 있다」 를 되짚을 자리가 여기 말고는 없다.
|
|
623
|
+
*/
|
|
624
|
+
case '단계옮김':
|
|
625
|
+
곁(` ${c.hcyan('→')} ${c.gray(옮긴말('ev.phaseMove', {
|
|
626
|
+
왜: 옮긴말(ev.왜 === '바꿈' ? 'ev.phaseWhyEdit' : 'ev.phaseWhyTodo'),
|
|
627
|
+
옛: 보일이름(ev.옛),
|
|
628
|
+
새: 보일이름(ev.새),
|
|
629
|
+
}))}`);
|
|
630
|
+
break;
|
|
631
|
+
|
|
632
|
+
// 바이트만 오고 내용이 안 오는 자리. 배치 기록에 남아야 「그날 밤 왜
|
|
633
|
+
// 12분이 걸렸나」 를 나중에 읽을 수 있다.
|
|
634
|
+
case '소식없음':
|
|
635
|
+
곁(` ${c.yellow('⧗')} ${c.gray(옮긴말('ev.noNews', { 초: ev.초 }))}`);
|
|
636
|
+
break;
|
|
637
|
+
|
|
615
638
|
case 'folded':
|
|
616
639
|
곁(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 (${ev.아낀토큰.toLocaleString()} 토큰을 비움)`)}`);
|
|
617
640
|
break;
|
package/src/repl.js
CHANGED
|
@@ -48,7 +48,7 @@ import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as
|
|
|
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';
|
|
51
|
-
import {
|
|
51
|
+
import { 잠잠기본, 무소식기본 } from './backend/http.js';
|
|
52
52
|
import { 인증서설정 } from './backend/clientcert.js';
|
|
53
53
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
54
54
|
import { expand as expandMentions } from './agent/mention.js';
|
|
@@ -249,6 +249,8 @@ export async function chatLoop(opts = {}) {
|
|
|
249
249
|
* 모았다가 한 번에 주는 사내 게이트웨이면 이 값을 올린다.
|
|
250
250
|
*/
|
|
251
251
|
잠잠: prof.잠잠 ?? prof.streamIdleMs ?? 잠잠기본,
|
|
252
|
+
// 바이트는 오는데 내용이 안 올 때의 전체 상한 (backend/http.js 의 무소식기본).
|
|
253
|
+
무소식: prof.무소식 ?? prof.streamNoNewsMs ?? 무소식기본,
|
|
252
254
|
// 게이트웨이가 우리 인증서를 요구하면 (mTLS, backend/clientcert.js).
|
|
253
255
|
// 파일 경로만 싣는다 — 알맹이는 요청 직전에 읽는다.
|
|
254
256
|
인증서: 인증서설정(prof),
|
|
@@ -2137,6 +2139,48 @@ export async function chatLoop(opts = {}) {
|
|
|
2137
2139
|
}
|
|
2138
2140
|
break;
|
|
2139
2141
|
|
|
2142
|
+
/*
|
|
2143
|
+
* 종합 모드가 **단계를 옮겼다** (agent/단계.js).
|
|
2144
|
+
*
|
|
2145
|
+
* 이 줄이 없으면 사람은 화면의 모드 표시가 턴 도중에 왜 바뀌었는지
|
|
2146
|
+
* 모른다 — 자기가 누른 적도 없는데 `~디버그` 가 `~코드` 로 바뀌어
|
|
2147
|
+
* 있으면 그건 알림이 아니라 버그처럼 보인다. 그래서 무엇을 보고
|
|
2148
|
+
* 옮겼는지(`왜`) 를 같이 적는다.
|
|
2149
|
+
*
|
|
2150
|
+
* 도구가 늘어난 경우는 한 줄 더 붙인다. 규격이 도구 목록을 앞머리
|
|
2151
|
+
* 맨 앞에 렌더하므로, 도구가 하나 늘면 그 턴은 앞머리를 캐시에서 못
|
|
2152
|
+
* 읽고 다시 쓴다. 접기(folded)에서 그 대가를 적기 시작한 것과 같은
|
|
2153
|
+
* 이유다 — 이득만 보이고 값은 며칠 뒤 청구서에서 보는 일이 없게.
|
|
2154
|
+
*/
|
|
2155
|
+
case '단계옮김': {
|
|
2156
|
+
const 왜 = 옮긴말(ev.왜 === '바꿈' ? 'ev.phaseWhyEdit' : 'ev.phaseWhyTodo');
|
|
2157
|
+
say(` ${c.hcyan('→')} ${c.gray(옮긴말('ev.phaseMove', { 왜, 옛: 보일이름(ev.옛), 새: 보일이름(ev.새) }))}`);
|
|
2158
|
+
if ((ev.늘어난도구?.length ?? 0) > 0 && session.level !== '쉬움') {
|
|
2159
|
+
const 보일수 = Math.min(5, ev.늘어난도구.length);
|
|
2160
|
+
const 이름들 = ev.늘어난도구.slice(0, 보일수).join(' · ')
|
|
2161
|
+
+ (ev.늘어난도구.length > 보일수 ? ` +${ev.늘어난도구.length - 보일수}` : '');
|
|
2162
|
+
say(` ${c.gray(옮긴말('ev.phaseTools', { 도구: 이름들 }))}`);
|
|
2163
|
+
}
|
|
2164
|
+
break;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/*
|
|
2168
|
+
* 바이트는 오는데 **내용이 안 온다** (backend/adapter.js).
|
|
2169
|
+
*
|
|
2170
|
+
* 여기가 없으면 화면에는 스피너만 돈다. 실제로 15분 넘게 그랬고,
|
|
2171
|
+
* 사람은 프로그램이 멎은 줄 알았다. 멎은 게 아니라 기다릴 이유가
|
|
2172
|
+
* 없어지는 순간이 안 온 것이라, 그걸 그대로 적어 준다.
|
|
2173
|
+
*
|
|
2174
|
+
* 끊지 않는다는 것도 같이 말한다 — 추론을 감추는 게이트웨이는 원래
|
|
2175
|
+
* 이 모양으로 정상 동작하므로, 사람이 기다릴지 ESC 할지 고르게 한다.
|
|
2176
|
+
*/
|
|
2177
|
+
case '소식없음':
|
|
2178
|
+
say(` ${c.yellow('⧗')} ${c.gray(옮긴말('ev.noNews', { 초: ev.초 }))}`);
|
|
2179
|
+
if (ev.상한초) {
|
|
2180
|
+
say(` ${c.gray(옮긴말('ev.noNewsCap', { 상한초: ev.상한초 }))}`);
|
|
2181
|
+
}
|
|
2182
|
+
break;
|
|
2183
|
+
|
|
2140
2184
|
case 'trimmed':
|
|
2141
2185
|
say(` ${c.gray(`(컨텍스트가 차서 오래된 대화 ${ev.dropped}개를 줄였습니다)`)}`);
|
|
2142
2186
|
break;
|