deel-local-cli 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +34 -7
- package/README.md +35 -7
- package/bin/deel.js +34 -1
- package/package.json +1 -1
- package/src/acp/serve.js +83 -17
- package/src/agent/commit.js +2 -2
- package/src/agent/compact.js +4 -4
- package/src/agent/donecheck.js +97 -0
- package/src/agent/grade.js +1 -1
- package/src/agent/loop.js +176 -13
- package/src/agent/memory.js +1 -1
- package/src/agent/models.js +1 -1
- package/src/agent/modes.js +3 -0
- package/src/agent/outschema.js +1 -1
- package/src/agent/pins.js +7 -2
- package/src/agent/project.js +1 -1
- package/src/agent/recall.js +1 -1
- package/src/agent/session.js +15 -0
- package/src/agent/store.js +13 -26
- package/src/backend/ctxsize.js +2 -2
- package/src/backend/http.js +1 -1
- package/src/backend/learn.js +27 -1
- package/src/backend/mcp.js +97 -16
- package/src/backend/proxy.js +1 -1
- package/src/backend/quota.js +2 -2
- package/src/commands.js +24 -2
- package/src/completion.js +16 -3
- package/src/config.js +33 -11
- package/src/configexplain.js +1 -1
- package/src/doctor.js +11 -1
- package/src/eval/run.js +432 -0
- package/src/eval/starter.js +95 -0
- package/src/i18n/en.js +20 -0
- package/src/i18n/ja.js +20 -0
- package/src/i18n/ko.js +20 -0
- package/src/i18n/zh.js +20 -0
- package/src/jsonl.js +42 -0
- package/src/lsp/client.js +6 -46
- package/src/oneshot.js +85 -3
- package/src/pack/selfpack.js +2 -2
- package/src/pack/zip.js +18 -1
- package/src/plugins/manage.js +81 -14
- package/src/reap.js +70 -0
- package/src/repl.js +85 -11
- package/src/report.js +1 -1
- package/src/safety/audit.js +4 -19
- package/src/safety/authcmd.js +1 -1
- package/src/safety/guard.js +56 -6
- package/src/safety/hooks.js +1 -1
- package/src/safety/keystore.js +18 -3
- package/src/safety/policy.js +38 -0
- package/src/safety/shellenv.js +1 -1
- package/src/safety/undo.js +44 -20
- package/src/skills/discover.js +1 -1
- package/src/tools/docs.js +3 -3
- package/src/tools/edit-match.js +1 -1
- package/src/tools/encoding.js +1 -1
- package/src/tools/excel-com.js +56 -30
- package/src/tools/fastgrep.js +1 -1
- package/src/tools/fsutil.js +8 -4
- package/src/tools/ignore.js +21 -1
- package/src/tools/index.js +122 -11
- package/src/tools/jobs.js +23 -4
- package/src/tools/shell.js +27 -0
- package/src/tools/todo.js +1 -1
- package/src/tools/webfetch.js +1 -1
- package/src/tools/xlsx.js +4 -4
- package/src/ui/approve.js +57 -3
- package/src/ui/banner.js +1 -1
- package/src/ui/inputbox.js +1 -1
- package/src/ui/md.js +1 -1
- package/src/ui/motion.js +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// 완료 검사 — 「다 됐습니다」 를 모델의 말이 아니라 **검사 결과**로 가른다 (2.1.0).
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 만드나 ───────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 사내 검토에서 나온 말이 둘이었다. 「검증을 정적 검사에 기대면 한계가 있다」 와
|
|
6
|
+
// 「루프를 더 다듬어야 한다」. 재 보니 맞았다. 턴이 끝나는 자리는 **모델이 도구를
|
|
7
|
+
// 안 부르고 말만 한 걸음** 하나였다. 모델이 「고쳤습니다」 라고 하면 그걸로 끝이었다.
|
|
8
|
+
//
|
|
9
|
+
// 확인할 길은 있었다. Verify 는 문법·짝을 읽어서 보고(tools/verify.js), checkmethods 는
|
|
10
|
+
// 이 프로젝트의 검사 명령을 찾아 알려 주고(tools/checkmethods.js), 증거는 「고친 뒤에
|
|
11
|
+
// 돌려서 통과한 검사가 있나」 를 사람에게 보여 준다(agent/evidence.js). 셋 다 **권하거나
|
|
12
|
+
// 보여 주기만** 한다. 돌릴지는 모델이 정했고, 안 돌리고 끝내도 막는 자리가 없었다.
|
|
13
|
+
//
|
|
14
|
+
// 그래서 사람이 검사 명령 하나를 정해 두면(`"check": "npm test"`), 모델이 끝내려는
|
|
15
|
+
// 자리에서 **deel 이 직접** 그 명령을 돌린다.
|
|
16
|
+
//
|
|
17
|
+
// 통과 그대로 끝낸다. 끝맺음에 「완료 검사 통과」 가 붙는다
|
|
18
|
+
// 실패 실패 출력을 모델에게 돌려주고 이어서 고치게 한다 — 정해 둔 판수까지
|
|
19
|
+
// 다 써도 실패 끝내되 **실패로** 끝낸다. `deel run` 은 종료코드 8 을 낸다
|
|
20
|
+
//
|
|
21
|
+
// ── 안 하는 것 ──────────────────────────────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// 명령을 **지어내지 않는다.** 정해 둔 것이 없으면 이 고리는 아예 안 돈다. 있을 법한
|
|
24
|
+
// 검사(npm test)를 짐작해 돌리면, 그 프로젝트에서 그게 20분짜리이거나 DB 를 비우는
|
|
25
|
+
// 스크립트일 때 사람이 모르는 채로 돈다.
|
|
26
|
+
//
|
|
27
|
+
// 따로 도는 길도 **안 만든다.** 이 명령은 모델이 부른 Bash 와 똑같은 관문을 지난다 —
|
|
28
|
+
// 적어 둔 금지 규칙, 사람이 건 훅, 승인 모드(strict 면 묻는다), 위험 명령 막이, 열쇠 뺀
|
|
29
|
+
// 환경. 설정에 적힌 명령이라고 관문을 건너뛰면 저장소 설정 한 줄이 strict 를 뚫는다.
|
|
30
|
+
//
|
|
31
|
+
// 아무것도 안 바꾼 턴에는 안 돈다. 물어본 것에 답만 한 턴에 테스트가 도는 것은 소음이다.
|
|
32
|
+
// 고친 뒤 실패를 받고 **다시 아무것도 안 바꾼 채** 끝내려 하면 다시 돌리지 않는다 —
|
|
33
|
+
// 같은 결과가 나올 것을 또 기다리게 하지 않고, 실패로 끝낸다.
|
|
34
|
+
|
|
35
|
+
/** 한 턴에 몇 번까지 「고치고 → 다시 검사」 를 돌까. 기본과 상한. */
|
|
36
|
+
export const 기본판수 = 3;
|
|
37
|
+
export const 최대판수 = 10;
|
|
38
|
+
/** 검사 한 번의 제한 시간(ms). Bash 도구의 상한(10분)과 같다 — 넘겨 봐야 거기서 잘린다. */
|
|
39
|
+
export const 기본시간 = 600_000;
|
|
40
|
+
const 최소시간 = 1_000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 설정과 실행 깃발에서 완료 검사를 읽는다. 정해 둔 명령이 없으면 null — 고리를 안 돈다.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} cfg 설정 (`check` · `checkRounds` · `checkTimeout`)
|
|
46
|
+
* @param {string} [덮을명령] `deel run --check` 로 준 명령. 설정보다 이긴다
|
|
47
|
+
* @returns {{명령:string, 판수:number, 시간:number} | null}
|
|
48
|
+
*/
|
|
49
|
+
export function 검사설정(cfg = {}, 덮을명령 = undefined) {
|
|
50
|
+
const 날것 = 덮을명령 !== undefined && 덮을명령 !== null ? 덮을명령 : cfg?.check;
|
|
51
|
+
if (typeof 날것 !== 'string') return null;
|
|
52
|
+
const 명령 = 날것.trim();
|
|
53
|
+
if (!명령) return null;
|
|
54
|
+
/*
|
|
55
|
+
* 판수는 1 ~ 10 에서만 받는다. 0 이나 음수를 「끈다」 로 읽으면 명령을 적어 둔 사람이
|
|
56
|
+
* 왜 안 도는지 모른다. 끄려면 check 를 지운다. 숫자가 아니면 기본값이다.
|
|
57
|
+
*/
|
|
58
|
+
const n = Number(cfg?.checkRounds);
|
|
59
|
+
const 판수 = Number.isInteger(n) && n >= 1 ? Math.min(n, 최대판수) : 기본판수;
|
|
60
|
+
const 초 = Number(cfg?.checkTimeout);
|
|
61
|
+
const 시간 = Number.isFinite(초) && 초 > 0 ? Math.min(기본시간, Math.max(최소시간, Math.round(초 * 1000))) : 기본시간;
|
|
62
|
+
return { 명령, 판수, 시간 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 긴 출력에서 **뒤를** 남긴다. 테스트가 무엇이 틀렸는지 말하는 자리는 대개 끝이다 —
|
|
67
|
+
* 앞을 남기면 모델은 「테스트 시작」 줄만 받고 무엇이 틀렸는지 모른다.
|
|
68
|
+
*/
|
|
69
|
+
export function 출력꼬리(글, 한도 = 6000) {
|
|
70
|
+
const s = String(글 ?? '');
|
|
71
|
+
if (s.length <= 한도) return s;
|
|
72
|
+
return `…(앞 ${s.length - 한도}자 줄임)\n${s.slice(s.length - 한도)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 도구 결과 하나를 통과·실패로 가른다. 종료코드 0 으로 끝난 것만 통과다 —
|
|
77
|
+
* 시그널로 죽은 것·시간 초과·못 돌린 것은 다 실패다 (tools/index.js 의 Bash 결과 머리말).
|
|
78
|
+
*/
|
|
79
|
+
export function 통과했나(result) {
|
|
80
|
+
return !!result && !result.error && result.failed !== true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 실패한 검사를 모델에게 돌려줄 말. 무엇을 돌렸고, 몇 번째이고, 무엇이 나왔는지. */
|
|
84
|
+
export function 실패말({ 명령, 출력, 판, 최대 }, { 영어 = false } = {}) {
|
|
85
|
+
const 남은 = Math.max(0, 최대 - 판);
|
|
86
|
+
const 꼬리 = 출력꼬리(출력);
|
|
87
|
+
if (영어) {
|
|
88
|
+
return `The completion check \`${명령}\` failed (${판}/${최대}). You said the work was done, but the check does not pass.\n\n`
|
|
89
|
+
+ `${꼬리}\n\n`
|
|
90
|
+
+ 'Read the failure, fix the cause in the code, and finish again. Do not edit or delete the check itself to make it pass.'
|
|
91
|
+
+ (남은 ? ` The check runs again when you finish (${남은} more ${남은 === 1 ? 'try' : 'tries'}).` : '');
|
|
92
|
+
}
|
|
93
|
+
return `완료 검사 \`${명령}\` 가 실패했습니다 (${판}/${최대}). 다 됐다고 했는데 검사가 통과하지 않습니다.\n\n`
|
|
94
|
+
+ `${꼬리}\n\n`
|
|
95
|
+
+ '실패 내용을 읽고 코드에서 원인을 고친 뒤 다시 끝내세요. 검사를 통과시키려고 검사 자체를 고치거나 지우지 마세요.'
|
|
96
|
+
+ (남은 ? ` 끝내면 검사가 다시 돕니다 (${남은}번 남음).` : '');
|
|
97
|
+
}
|
package/src/agent/grade.js
CHANGED
package/src/agent/loop.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// 화면에 그릴 것은 이벤트로 흘려보낸다 — 화면 코드와 섞지 않는다.
|
|
3
3
|
import { chat, chatStream, assistantMessage, toolMessage, 말없이끝남, 흐름멎음, 보낸토큰, 부른것들, 도구결과인가 } from '../backend/adapter.js';
|
|
4
4
|
import { 그림메시지 } from '../backend/vision.js';
|
|
5
|
-
import {
|
|
5
|
+
import { 어떻게할까, 승인바닥 } from '../safety/policy.js';
|
|
6
6
|
import { 자리돌리기, 막힘말 } from '../safety/hooks.js';
|
|
7
|
-
import { toolSchemas, runTool, TOOLS,
|
|
8
|
-
import { isMutating } from '../safety/guard.js';
|
|
7
|
+
import { toolSchemas, runTool, TOOLS, 파일현황, 바뀔내용 } from '../tools/index.js';
|
|
8
|
+
import { isMutating, 셸이파일에쓰나 } from '../safety/guard.js';
|
|
9
9
|
import { effortFor, tokensFor, fullCap, wasCut, shiftLevel, 자동강도, 천장고르기, 인사인가 as 인사말인가 } from './effort.js';
|
|
10
10
|
import { 배울전선, 카드고치기, 카드저장꼴, 전선붙이기 } from '../backend/wire.js';
|
|
11
11
|
import { 살린쓰기 } from './salvage.js';
|
|
@@ -19,13 +19,14 @@ import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기
|
|
|
19
19
|
import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
20
20
|
import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴까 } from '../safety/secrets.js';
|
|
21
21
|
import { 바깥인가 } from '../safety/runmode.js';
|
|
22
|
-
import { get as workMode } from './modes.js';
|
|
22
|
+
import { get as workMode, 바꾸는도구 } from './modes.js';
|
|
23
23
|
// 종합 모드에서 단계가 일을 따라간다 (agent/phase.js).
|
|
24
24
|
import { 다음단계 } from './phase.js';
|
|
25
25
|
import { 묻지말라했나, 손대라했나 } from './route.js';
|
|
26
26
|
import { 지시말 } from '../i18n/index.js';
|
|
27
27
|
import { 빠진것, 빠졌다는말 } from './asks.js';
|
|
28
28
|
import { 환경속열쇠들 } from '../config.js';
|
|
29
|
+
import { 통과했나, 실패말 as 검사실패말, 출력꼬리 } from './donecheck.js';
|
|
29
30
|
|
|
30
31
|
/*
|
|
31
32
|
* 콜백으로만 소식을 주는 부름을, 제너레이터가 중간에 내보낼 수 있는 모양으로 바꾼다.
|
|
@@ -148,6 +149,13 @@ function thinkFor(conn, level) {
|
|
|
148
149
|
*/
|
|
149
150
|
const 읽기전용 = new Set(['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'Recall', 'Skill', 'WebFetch']);
|
|
150
151
|
|
|
152
|
+
/*
|
|
153
|
+
* 스크립트 **파일**을 돌리는 꼴 — `python fix.py` · `node update.mjs` · `sh gen.sh`.
|
|
154
|
+
* 완료 검사가 「이번 턴에 무엇을 바꿨나」 를 셀 때만 쓴다. 스크립트 안에서 무엇을 쓰는지는
|
|
155
|
+
* 명령줄로 모르니 **바꿨을 수 있다**로 친다. 잘못 걸리면 검사가 한 번 더 돌 뿐이다.
|
|
156
|
+
*/
|
|
157
|
+
const 스크립트돌림 = /\b(?:node|python[\d.]*|py|ruby|perl|php|deno|bun|bash|sh|pwsh|powershell)(?:\.exe)?\s+(?:-\S+\s+)*[^\s|;&]+\.(?:[cm]?js|ts|py|rb|pl|php|sh|ps1)\b/i;
|
|
158
|
+
|
|
151
159
|
/*
|
|
152
160
|
* 잘린 답을 「생각을 줄여」 다시 부를 때 쓰는 두 값.
|
|
153
161
|
*
|
|
@@ -160,8 +168,8 @@ const 읽기전용 = new Set(['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs',
|
|
|
160
168
|
* 2,400토큰이고 2k 상한에서는 300토큰이다. 같은 잣대인 척하면서 실제로는
|
|
161
169
|
* 상한이 클수록 헐거워진다.
|
|
162
170
|
*/
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
const 되찾을몫 = 0.4;
|
|
172
|
+
const 쓸모있는자리 = 1024;
|
|
165
173
|
|
|
166
174
|
/**
|
|
167
175
|
* 호출 목록을 '같이 돌려도 되는 덩어리' 로 자른다.
|
|
@@ -337,6 +345,99 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
337
345
|
*/
|
|
338
346
|
const 손댄파일 = new Set();
|
|
339
347
|
const 마무리 = () => [...손댄파일].map(파일현황);
|
|
348
|
+
/*
|
|
349
|
+
* 완료 검사 (agent/donecheck.js). 사람이 검사 명령을 정해 뒀으면 모델이 끝내려는 자리에서
|
|
350
|
+
* 그걸 돌린다. 하위 작업은 안 돈다 — 한 번 시킨 일에 검사가 하위 수만큼 돈다.
|
|
351
|
+
*
|
|
352
|
+
* 「고친 뒤에 다시 돌릴까」 는 바뀐 **횟수**로 가른다. 손댄파일 은 이름 모음이라 같은
|
|
353
|
+
* 파일을 다시 고치면 안 늘어난다 — 실패를 받고 같은 파일을 고친 판이 「안 바꿨다」 로
|
|
354
|
+
* 읽혀 검사가 다시 안 돈다.
|
|
355
|
+
*/
|
|
356
|
+
const 완료검사 = 깊이 ? null : (ctx.완료검사 ?? null);
|
|
357
|
+
let 바뀜수 = 0;
|
|
358
|
+
let 검사때바뀜수 = 0;
|
|
359
|
+
let 검사판 = 0;
|
|
360
|
+
let 검사상태 = null;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 완료 검사를 한 번 돌린다. 모델이 부른 Bash 와 **같은 관문**을 지난다 — 적어 둔 규칙,
|
|
364
|
+
* 사람이 건 훅, 승인 모드, 그리고 Bash 도구 안의 위험 명령 막이·열쇠 뺀 환경.
|
|
365
|
+
* 설정에 적힌 명령이라고 건너뛰면 저장소 설정 한 줄이 strict 를 뚫는다.
|
|
366
|
+
*
|
|
367
|
+
* @returns {{ok: boolean|null, 명령, 판, 최대, 요약?, 까닭?, 출력?}} ok 가 null 이면 못 돌렸다
|
|
368
|
+
*/
|
|
369
|
+
async function* 완료검사돌리기(판) {
|
|
370
|
+
const 명령 = 완료검사.명령;
|
|
371
|
+
const 최대 = 완료검사.판수;
|
|
372
|
+
const args = { command: 명령, description: '완료 검사', timeout: 완료검사.시간 };
|
|
373
|
+
yield { type: 'check_start', 명령, 판, 최대 };
|
|
374
|
+
const 못돌림 = (까닭) => ({ ok: null, 명령, 판, 최대, 까닭 });
|
|
375
|
+
|
|
376
|
+
let 결판 = null;
|
|
377
|
+
const 판정 = 어떻게할까(ctx.규칙들, 'Bash', args, { 뿌리: ctx.scope?.root ?? null });
|
|
378
|
+
if (판정.답 === 'deny') {
|
|
379
|
+
ctx.audit?.blocked?.('규칙으로 금지됨', `${판정.출처}: ${판정.규칙}`);
|
|
380
|
+
결판 = 못돌림(`${판정.출처}에 적힌 규칙 ${판정.규칙} 으로 막혀 있습니다`);
|
|
381
|
+
}
|
|
382
|
+
if (!결판 && ctx.훅들?.length) {
|
|
383
|
+
const 훅 = await 자리돌리기(ctx.훅들, '도구전', {
|
|
384
|
+
도구: 'Bash', 넣을것: { 인자: args }, signal, audit: ctx.audit,
|
|
385
|
+
});
|
|
386
|
+
if (훅.막힘) {
|
|
387
|
+
ctx.audit?.blocked?.('도구전 훅이 막음', `Bash · ${훅.막힘.훅.명령}`);
|
|
388
|
+
결판 = 못돌림(막힘말(훅.막힘));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!결판) {
|
|
392
|
+
// 아래 도구 관문의 needsOk 와 같은 잣대다. 검사 명령은 대개 안 바꾸는 명령이라 confirm 은 안 묻는다.
|
|
393
|
+
const 정책이묻게함 = 승인바닥().바닥 !== 'auto';
|
|
394
|
+
const 물어야 = (판정.답 === 'allow' && !정책이묻게함) ? false
|
|
395
|
+
: session.mode === 'strict' ? true
|
|
396
|
+
: session.mode === 'confirm' ? isMutating(명령) : false;
|
|
397
|
+
if (물어야) {
|
|
398
|
+
const ok = ctx.confirm ? await ctx.confirm('Bash', args, { 미리보기: [] }) : false;
|
|
399
|
+
if (!ok) 결판 = 못돌림(ctx.confirm ? '사람이 거절했습니다' : '승인이 필요한데 물어볼 사람이 없습니다');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!결판) {
|
|
403
|
+
let result;
|
|
404
|
+
try { result = await runTool('Bash', args, ctx); }
|
|
405
|
+
catch (err) { result = { error: String(err?.message ?? err) }; }
|
|
406
|
+
const 아는열쇠들 = [conn.key, ...환경속열쇠들()].filter(Boolean);
|
|
407
|
+
if (result.중단됨) 결판 = 못돌림('중단했습니다');
|
|
408
|
+
/*
|
|
409
|
+
* Bash 도구 안의 위험 명령 막이·경로 막이에 걸렸다(`막힘 — …`). 이것도 **못 돌린** 것이다.
|
|
410
|
+
* 실패로 치면 모델에게 「검사를 통과시키라」 고 되미는데, 막힌 것은 모델이 고칠 수 있는 일이
|
|
411
|
+
* 아니다 — 판만 쓰고 같은 자리에서 또 막힌다 (2차 눈 판정).
|
|
412
|
+
*/
|
|
413
|
+
else if (/^막힘 — /.test(String(result.error ?? ''))) 결판 = 못돌림(String(result.error));
|
|
414
|
+
else {
|
|
415
|
+
let 출력 = String(result.content ?? result.error ?? '');
|
|
416
|
+
// 모델에게 돌려줄 글이다 — 도구 결과와 같은 자로 가린다 (아래 도구 결과 싣는 자리).
|
|
417
|
+
if (가릴까('Bash', { 바깥: 바깥으로나감 })) {
|
|
418
|
+
const 가린 = 가리기(출력, { 열쇠들: 아는열쇠들 });
|
|
419
|
+
if (가린.가린것.length) 출력 = 가린.글 + 가렸다는말(가린.가린것);
|
|
420
|
+
}
|
|
421
|
+
결판 = {
|
|
422
|
+
ok: 통과했나(result), 명령, 판, 최대, 출력,
|
|
423
|
+
요약: String(result.summary ?? result.error ?? '').split('\n')[0],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// 도구전 훅을 지났으면 도구후 훅도 지난다 — 감사 훅이 앞만 보고 뒤를 못 보면 반쪽 기록이다.
|
|
427
|
+
if (ctx.훅들?.length && !result.중단됨) {
|
|
428
|
+
const 훅 = await 자리돌리기(ctx.훅들, '도구후', {
|
|
429
|
+
도구: 'Bash', 넣을것: { 인자: args, 실패: 결판.ok !== true, 바뀐것: null }, signal, audit: ctx.audit,
|
|
430
|
+
});
|
|
431
|
+
if (훅.말들.length) {
|
|
432
|
+
const 가린 = 가리기(훅.말들.join('\n'), { 열쇠들: 아는열쇠들 });
|
|
433
|
+
for (const 말 of 가린.글.split('\n')) yield { type: 'hook_note', 자리: '도구후', 도구: 'Bash', 말 };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const { 출력: _뺌, ...보일것 } = 결판;
|
|
438
|
+
yield { type: 'check', ...보일것, 꼬리: 결판.출력 ? 출력꼬리(결판.출력, 1200) : '' };
|
|
439
|
+
return 결판;
|
|
440
|
+
}
|
|
340
441
|
|
|
341
442
|
// 살려 쓴 파일이 지난번보다 실제로 커졌는지 보려고 크기를 기억해 둔다.
|
|
342
443
|
const 살린크기 = new Map();
|
|
@@ -1536,6 +1637,30 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1536
1637
|
yield { type: 'nudge', why: '요청누락', 빠진, text: msg.content };
|
|
1537
1638
|
continue;
|
|
1538
1639
|
}
|
|
1640
|
+
/*
|
|
1641
|
+
* ── 완료 검사 (agent/donecheck.js) ────────────────────────────────
|
|
1642
|
+
*
|
|
1643
|
+
* 모델은 「다 됐습니다」 라고 했다. 사람이 검사 명령을 정해 뒀고 이번 턴에 무엇을
|
|
1644
|
+
* 바꿨으면, 그 말을 믿기 전에 검사를 돌린다. 실패하면 출력을 돌려주고 이어서
|
|
1645
|
+
* 고치게 한다 — 정해 둔 판수까지. 다 쓰고도 실패면 **실패로** 끝낸다(done 의 검사).
|
|
1646
|
+
*
|
|
1647
|
+
* 지난 검사 뒤로 바뀐 것이 없으면 다시 안 돌린다. 같은 결과를 또 기다리게 할 뿐이다.
|
|
1648
|
+
* 못 돌린 것(규칙 금지·훅·승인 거절)은 되밀지 않는다 — 모델이 고칠 수 있는 일이 아니다.
|
|
1649
|
+
*/
|
|
1650
|
+
if (완료검사 && 바뀜수 > 검사때바뀜수 && 검사판 < 완료검사.판수) {
|
|
1651
|
+
검사판++;
|
|
1652
|
+
검사때바뀜수 = 바뀜수;
|
|
1653
|
+
검사상태 = yield* 완료검사돌리기(검사판);
|
|
1654
|
+
if (검사상태.ok === false && 검사판 < 완료검사.판수) {
|
|
1655
|
+
// 명령 줄도 모델에게 가는 글이다. 출력만 가리고 `--token=…` 을 날것으로 싣으면 반쪽이다.
|
|
1656
|
+
const 명령줄 = 가리기(검사상태.명령, { 열쇠들: [conn.key, ...환경속열쇠들()].filter(Boolean) }).글;
|
|
1657
|
+
session.push({
|
|
1658
|
+
role: 'user',
|
|
1659
|
+
content: 검사실패말({ 명령: 명령줄, 출력: 검사상태.출력, 판: 검사판, 최대: 완료검사.판수 }, { 영어: 지시말() === 'en' }),
|
|
1660
|
+
});
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1539
1664
|
/*
|
|
1540
1665
|
* ── 턴이 끝나는 자리 (safety/hooks.js) ────────────────────────────
|
|
1541
1666
|
*
|
|
@@ -1560,7 +1685,9 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1560
1685
|
}
|
|
1561
1686
|
}
|
|
1562
1687
|
// 밀고도 그대로면 조용히 넘어가지 않는다. 사람이 알아야 다음을 정한다.
|
|
1563
|
-
|
|
1688
|
+
// 완료 검사를 돌렸으면 그 결과도 싣는다 — `deel run` 이 종료코드를, 화면이 끝맺음 줄을 여기서 정한다.
|
|
1689
|
+
const 검사 = 검사상태 && (({ 출력: _뺌, ...나머지 }) => 나머지)(검사상태);
|
|
1690
|
+
yield { type: 'done', steps, text: msg.content, files: 마무리(), 빠진, 검사 };
|
|
1564
1691
|
return;
|
|
1565
1692
|
}
|
|
1566
1693
|
|
|
@@ -1894,16 +2021,40 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1894
2021
|
for (const 말 of 훅.말들) yield { type: 'hook_note', 자리: '도구전', 도구: call.name, 말 };
|
|
1895
2022
|
}
|
|
1896
2023
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2024
|
+
/*
|
|
2025
|
+
* 모드에 따라 물어본다. 기본(auto)은 안 묻고 되돌리기로 대응한다.
|
|
2026
|
+
*
|
|
2027
|
+
* strict 는 **바꾸는 도구를 다** 묻는다. 여태 여기는 Write·Edit·Bash 셋만 적혀 있어서
|
|
2028
|
+
* Append·Move·Jobs 는 strict 에서도 안 묻고 돌았다 — modes.js 가 바꾸는 도구를 여섯으로
|
|
2029
|
+
* 적어 두고, 이 자리만 옛 목록에 남아 있었다. MCP 도구는 남의 프로그램이라 무엇을 바꾸는지
|
|
2030
|
+
* 우리가 모른다. 모를 때는 묻는다.
|
|
2031
|
+
*
|
|
2032
|
+
* 관리 정책이 승인 바닥을 걸었으면(policy.js 의 승인바닥) 사람이 설정에 적어 둔 「늘 허락」
|
|
2033
|
+
* 도 묻기를 못 건너뛴다. 정책은 설정을 이긴다 — 관리자가 「바꾸기 전에 늘 묻는다」 를
|
|
2034
|
+
* 걸었는데 `"allow": ["Edit"]` 한 줄로 풀리면 그 바닥은 장식이다.
|
|
2035
|
+
*/
|
|
2036
|
+
const 정책이묻게함 = 승인바닥().바닥 !== 'auto';
|
|
2037
|
+
// Jobs 는 대개 뒤에서 도는 명령의 출력을 **읽는** 호출이다. 끝내기(stop)만 바꾸는 것으로
|
|
2038
|
+
// 친다 — 읽을 때마다 물으면 사람은 y 만 치다가 정작 물어야 할 것도 그렇게 넘긴다.
|
|
2039
|
+
const 바꾸는것 = (call.name === 'Jobs' ? !!call.args?.stop : 바꾸는도구.includes(call.name))
|
|
2040
|
+
|| call.name.startsWith('mcp__');
|
|
2041
|
+
const needsOk = (판정.답 === 'allow' && !정책이묻게함) ? false : session.mode === 'strict'
|
|
2042
|
+
? 바꾸는것
|
|
1900
2043
|
: session.mode === 'confirm'
|
|
1901
2044
|
? (call.name === 'Bash' && isMutating(call.args?.command))
|
|
1902
2045
|
: false;
|
|
1903
|
-
if (needsOk
|
|
1904
|
-
|
|
2046
|
+
if (needsOk) {
|
|
2047
|
+
/*
|
|
2048
|
+
* 물어볼 사람이 없으면 **거절**이다. 여태 `needsOk && ctx.confirm` 이라, confirm 을 안 단
|
|
2049
|
+
* 부르는 쪽에서는 strict 가 물음 없이 그대로 실행했다 — 「묻는다」 가 「물을 곳이 있으면
|
|
2050
|
+
* 묻는다」 였다. 승인은 답한 사람이 있을 때만 난다 (repl.js 의 confirm 머리말과 같은 자세).
|
|
2051
|
+
*/
|
|
2052
|
+
const 미리보기 = 바뀔내용(call.name, call.args, ctx);
|
|
2053
|
+
const ok = ctx.confirm ? await ctx.confirm(call.name, call.args, { 미리보기 }) : false;
|
|
1905
2054
|
if (!ok) {
|
|
1906
|
-
거절(call,
|
|
2055
|
+
거절(call, ctx.confirm
|
|
2056
|
+
? '사용자가 거부했습니다. 다른 방법을 찾거나 이유를 물어보세요.'
|
|
2057
|
+
: '승인이 필요한데 물어볼 사람이 없어 하지 않았습니다. 승인 없이 되는 방법을 고르세요.');
|
|
1907
2058
|
// 거부를 못 알아듣고 같은 것을 계속 물으면 사람만 계속 n 을 치게 된다.
|
|
1908
2059
|
if (막힘셈(call, '사용자 거부')) 멈출까 = '거부하셨는데도 같은 것을 계속 물어보고 있습니다';
|
|
1909
2060
|
yield { type: 'tool', name: call.name, args: call.args, result: { error: '거부됨' }, showLabel: true };
|
|
@@ -2141,6 +2292,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
2141
2292
|
* 여기서 또 합치면 같은 변경이 두 번 세어진다.
|
|
2142
2293
|
*/
|
|
2143
2294
|
for (const f of 끝?.files ?? []) if (f?.path) 손댄파일.add(f.path);
|
|
2295
|
+
if (끝?.files?.length) 바뀜수++;
|
|
2144
2296
|
// 하위가 쓴 토큰·시간도 이번 턴의 셈에 들어가야 한다. 안 그러면 /context 가 거짓말을 한다.
|
|
2145
2297
|
session.usage.in += 자식.usage.in;
|
|
2146
2298
|
session.usage.out += 자식.usage.out;
|
|
@@ -2282,6 +2434,17 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
2282
2434
|
if (result.changed || result.여럿?.some((f) => f.ok) || result.바뀐것들?.length) {
|
|
2283
2435
|
이번턴본것.바꿈 = true;
|
|
2284
2436
|
}
|
|
2437
|
+
/*
|
|
2438
|
+
* 완료 검사가 「고친 뒤에 다시 돌릴까」 를 이걸로 가른다. 셸로 바꾼 것(sed -i · mv ·
|
|
2439
|
+
* `> 파일` · 스크립트 파일 돌리기)도 센다 — 실패를 받고 셸로만 고친 판에서 검사가 다시
|
|
2440
|
+
* 안 돌면 고쳐 놓고도 실패로 끝난다. **검사가 한 번 돈 뒤에는** 셸·MCP 호출을 무엇이든
|
|
2441
|
+
* 센다(2차 눈 판정: `make` · `npx codemod` 는 어느 목록에도 없다). 고치는 중이라
|
|
2442
|
+
* 헛짚어도 검사가 한 번 더 돌 뿐이고, 판수가 끝을 막는다.
|
|
2443
|
+
*/
|
|
2444
|
+
const 셸명령 = call.name === 'Bash' ? String(call.args?.command ?? '') : null;
|
|
2445
|
+
if (!result.error && (result.changed || result.여럿?.some((f) => f.ok) || result.바뀐것들?.length
|
|
2446
|
+
|| (셸명령 !== null && (isMutating(셸명령) || 셸이파일에쓰나(셸명령) || 스크립트돌림.test(셸명령)))
|
|
2447
|
+
|| (검사판 > 0 && (셸명령 !== null || call.name.startsWith('mcp__'))))) 바뀜수++;
|
|
2285
2448
|
if (call.name === 'TodoWrite' && !result.error) 이번턴본것.할일 = true;
|
|
2286
2449
|
// 한 번에 여러 개를 쓴 경우. changed 하나만 보면 나머지가 조용히 빠져서,
|
|
2287
2450
|
// 턴 끝에 "만들어졌다" 고 확인해 주는 파일이 넷 중 하나만 나온다.
|
package/src/agent/memory.js
CHANGED
|
@@ -71,7 +71,7 @@ const 머리말 = `# 기억
|
|
|
71
71
|
* 남의 줄이 든 파일에 한 줄 **더해서** 쓸 때는 지문을 안 적는다. 적으면 남의
|
|
72
72
|
* 줄이 내 줄 틈에 끼어 「이 PC 가 적은 것」 으로 세탁된다.
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
const 내것표자리 = (env = process.env) => join(dirname(신뢰자리(env)), 'memory-own.json');
|
|
75
75
|
/** 지문을 들고 있는 폴더 수. 넘으면 오래 안 쓴 것부터 뺀다 — 빠진 폴더는 믿기 전까지 안 실릴 뿐이다. */
|
|
76
76
|
const 내것표최대 = 200;
|
|
77
77
|
/*
|
package/src/agent/models.js
CHANGED
package/src/agent/modes.js
CHANGED
|
@@ -36,6 +36,9 @@ const 계획 = ['TodoWrite', 'Remember'];
|
|
|
36
36
|
// Move 는 구조를 바꾸는 유일한 길이다. 이게 없으면 "폴더를 역할대로 나눠 줘"
|
|
37
37
|
// 에 남는 길이 Bash mv 뿐인데, 그건 매번 승인을 묻고 되돌리기에 안 잡힌다.
|
|
38
38
|
const 쓰기 = ['Write', 'Append', 'Edit', 'Move', 'Bash', 'Jobs'];
|
|
39
|
+
// strict 승인이 묻는 도구도 이 목록이다 (agent/loop.js). 따로 적어 두면 여기 도구가
|
|
40
|
+
// 늘 때 관문 쪽 목록이 뒤에 남는다 — Append·Move·Jobs 가 실제로 그렇게 빠져 있었다.
|
|
41
|
+
export const 바꾸는도구 = Object.freeze([...쓰기]);
|
|
39
42
|
// 만든 것을 확인하는 도구. 아무것도 안 바꾸지만 **쓰는 모드에만** 준다 —
|
|
40
43
|
// 안 만든 모드에서 확인할 것이 없고, 도구 정의로 나가는 자리만 먹는다.
|
|
41
44
|
const 확인 = ['Verify'];
|
package/src/agent/outschema.js
CHANGED
|
@@ -210,7 +210,7 @@ function 제자리참조찾기(뿌리) {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/** 같은 파일 안(`#`으로 시작)이 아닌 `$ref` 를 다 찾는다. */
|
|
213
|
-
|
|
213
|
+
function 바깥ref찾기(스키마, 본것 = new Set()) {
|
|
214
214
|
const out = [];
|
|
215
215
|
const 훑기 = (s) => {
|
|
216
216
|
if (!s || typeof s !== 'object' || 본것.has(s)) return;
|
package/src/agent/pins.js
CHANGED
|
@@ -43,9 +43,14 @@ export class 못박기 {
|
|
|
43
43
|
this.것들 = this.것들.slice(0, 최대개수);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/*
|
|
47
|
+
* 견줄 때는 NFC 로 맞춘다 (2.0.2 · 429). 맥에서 친 한글은 자모로 풀린 꼴(NFD)로 오기도 해서,
|
|
48
|
+
* 같은 「운영 DB 는 건드리지 마라」 가 두 번 박혔다 — 매 턴 실리는 자리를 같은 말이 둘 먹는다.
|
|
49
|
+
*/
|
|
46
50
|
#이미있나(말) {
|
|
47
|
-
const
|
|
48
|
-
|
|
51
|
+
const 열쇠 = (x) => x.normalize('NFC').toLowerCase();
|
|
52
|
+
const 낮 = 열쇠(말);
|
|
53
|
+
return this.것들.some((x) => 열쇠(x) === 낮);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
package/src/agent/project.js
CHANGED
package/src/agent/recall.js
CHANGED
|
@@ -23,7 +23,7 @@ import { sessionsDir } from './store.js';
|
|
|
23
23
|
import { 부른것들, 결과들, 도구결과인가, 본문글 } from '../backend/adapter.js';
|
|
24
24
|
|
|
25
25
|
// 한 번 찾을 때 읽을 최대 바이트. 넘으면 거기서 멈추고 **멈췄다고 말한다.**
|
|
26
|
-
|
|
26
|
+
const 읽기예산 = 64 * 1024 * 1024;
|
|
27
27
|
// 한 메시지에서 볼 최대 길이. 파일을 통째로 붙인 도구 결과가 수 MB 씩 있다.
|
|
28
28
|
const 메시지최대 = 200 * 1024;
|
|
29
29
|
|
package/src/agent/session.js
CHANGED
|
@@ -7,6 +7,7 @@ import { 조각표 } from '../backend/cachemark.js';
|
|
|
7
7
|
import { get as workMode, 말 as 모드말, DEFAULT as WORK_DEFAULT } from './modes.js';
|
|
8
8
|
import { toolSchemas } from '../tools/index.js';
|
|
9
9
|
import { isOffline } from '../safety/network.js';
|
|
10
|
+
import { 승인바닥, 더센승인 } from '../safety/policy.js';
|
|
10
11
|
import { normalize as normLevel, DEFAULT as LEVEL_DEFAULT } from '../ui/level.js';
|
|
11
12
|
import { 매김, 급말, 값 as 급값, 지켜본것 } from './grade.js';
|
|
12
13
|
import { 지문 } from './project.js';
|
|
@@ -218,6 +219,20 @@ function 기본규칙(ctx) {
|
|
|
218
219
|
const 규칙최대 = 20000;
|
|
219
220
|
|
|
220
221
|
export class Session {
|
|
222
|
+
#승인 = 'auto';
|
|
223
|
+
|
|
224
|
+
/*
|
|
225
|
+
* 승인 모드는 관리 정책의 바닥 아래로 못 내려간다 (safety/policy.js 의 승인바닥).
|
|
226
|
+
*
|
|
227
|
+
* 읽는 자리에서 덮는다. `/mode` · Shift+Tab · 편집기 · `deel run` · 하위 작업이 제각각
|
|
228
|
+
* session.mode 를 쓰는데, 자리마다 바닥을 걸면 하나를 빠뜨린 자리가 곧 구멍이다.
|
|
229
|
+
* 여기 한 곳에서 걸면 누가 무엇을 적든 관문(agent/loop.js)이 읽는 값은 바닥 위다.
|
|
230
|
+
*/
|
|
231
|
+
get mode() { return 더센승인(this.#승인, 승인바닥().바닥); }
|
|
232
|
+
set mode(v) { this.#승인 = v; }
|
|
233
|
+
/** 사람이 고른 그대로 — 바닥이 덮었는지를 화면이 말할 때 쓴다. */
|
|
234
|
+
get 고른승인() { return this.#승인; }
|
|
235
|
+
|
|
221
236
|
constructor(conn, { root, mode = 'auto', work = null, level = null, think = 'medium', effort = 'save', web = true, maxSteps = null } = {}) {
|
|
222
237
|
this.conn = conn;
|
|
223
238
|
this.root = root;
|
package/src/agent/store.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
import {
|
|
16
16
|
existsSync, mkdirSync, readdirSync, readFileSync,
|
|
17
17
|
appendFileSync, writeFileSync, statSync, rmSync, chmodSync, renameSync,
|
|
18
|
-
openSync, readSync, closeSync,
|
|
19
18
|
} from 'node:fs';
|
|
19
|
+
import { 줄로끝나나, 본인만잠그기 } from '../jsonl.js';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { 도구결과인가 } from '../backend/adapter.js';
|
|
22
22
|
import { BOM떼기 } from '../safety/trust.js';
|
|
@@ -131,9 +131,15 @@ export class Store {
|
|
|
131
131
|
* 그래서 「안 적히고 있습니다」 가 한 번도 안 떴다. 사람이 알아차리는 자리는
|
|
132
132
|
* 또 **다음 날 --resume** 이다 — 이 파일 위쪽이 너무 늦다고 적어 둔 그 자리다.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* 내가 적어 둔 데까지의 바이트 수를 들고 있다가, 적기 전에 파일이 그보다 자라
|
|
135
|
+
* 있으면 남이 쓴 것이다.
|
|
136
|
+
*
|
|
137
|
+
* **우리가 지은 이름의 창도 본다** (2.0.2 · 527). 지은 이름은 freeId 와 'wx' 가
|
|
138
|
+
* 「두 창이 같은 새 이름을 집는 것」 은 막지만, 다른 창이 `--continue` 로 **이 파일을**
|
|
139
|
+
* 집어 오는 것은 못 막는다 — 제일 최근 대화가 바로 이것이다. 그래서 뒤에 연 창만 멈추고
|
|
140
|
+
* 먼저 연 창은 그 창의 줄 사이에 제 줄을 섞어 넣었고, 접으면(replace) 그 창의 줄을
|
|
141
|
+
* 통째로 지웠다. 이제 누가 먼저 적든 **먼저 적은 쪽이 이어 가고 다른 쪽이 멈춘다** —
|
|
142
|
+
* 한 파일에 두 대화가 섞이는 일이 없다. 멈춘 쪽 화면은 「안 적히고 있습니다」 를 말한다.
|
|
137
143
|
*
|
|
138
144
|
* 그러면 **안 적고 센다.** 그냥 이어 붙이는 것은 두 대화를 섞어 놓는 일이고,
|
|
139
145
|
* 섞인 파일은 둘 다 못 쓰게 된다. 대신 잠자코 물러나지는 않는다 — 못쓴 셈에
|
|
@@ -144,7 +150,7 @@ export class Store {
|
|
|
144
150
|
/** 지금 파일 크기. 못 재면 null — 모르는 것은 모른다고 둔다(헛경보가 더 나쁘다). */
|
|
145
151
|
#크기() { try { return statSync(this.file).size; } catch { return null; } }
|
|
146
152
|
#남이썼나() {
|
|
147
|
-
if (this
|
|
153
|
+
if (this.#끝자리 == null) return false;
|
|
148
154
|
const 지금 = this.#크기();
|
|
149
155
|
return 지금 != null && 지금 !== this.#끝자리;
|
|
150
156
|
}
|
|
@@ -167,14 +173,7 @@ export class Store {
|
|
|
167
173
|
#잠그기() {
|
|
168
174
|
if (this.#잠갔나) return;
|
|
169
175
|
this.#잠갔나 = true;
|
|
170
|
-
|
|
171
|
-
* 윈도우에서 chmod 는 **아무 일도 안 하고 성공한다.** 그 성공을 그대로
|
|
172
|
-
* 적으면 바로 위 머리말이 스스로 금지한 「잠근 척」 이 된다 — 같은 PC 를
|
|
173
|
-
* 여럿이 쓰는 사람이 화면만 보고 안심한다. 안 걸었으면 안 걸었다고 적는다.
|
|
174
|
-
*/
|
|
175
|
-
if (process.platform === 'win32') { this.잠금 = { 못함: 'windows' }; return; }
|
|
176
|
-
try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
|
|
177
|
-
catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
|
|
176
|
+
this.잠금 = 본인만잠그기(this.file);
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
#open() {
|
|
@@ -274,18 +273,6 @@ export class Store {
|
|
|
274
273
|
* 파일마다 처음 적을 때 한 번, 끝이 개행인지 보고 아니면 개행부터 붙인다.
|
|
275
274
|
*/
|
|
276
275
|
#줄끝봤나 = false;
|
|
277
|
-
#줄로끝나나() {
|
|
278
|
-
let fd = null;
|
|
279
|
-
try {
|
|
280
|
-
const 크기 = statSync(this.file).size;
|
|
281
|
-
if (!크기) return true;
|
|
282
|
-
fd = openSync(this.file, 'r');
|
|
283
|
-
const 한바이트 = Buffer.alloc(1);
|
|
284
|
-
readSync(fd, 한바이트, 0, 1, 크기 - 1);
|
|
285
|
-
return 한바이트[0] === 0x0a;
|
|
286
|
-
} catch { return true; } // 없거나 못 읽으면 붙일 반쪽도 없다
|
|
287
|
-
finally { if (fd != null) { try { closeSync(fd); } catch { /* 닫다 터져도 적기는 한다 */ } } }
|
|
288
|
-
}
|
|
289
276
|
|
|
290
277
|
/** 한 줄 적는다. 적었으면 true. 못 적었으면 false 이고 셈에 오른다. */
|
|
291
278
|
#write(obj) {
|
|
@@ -296,7 +283,7 @@ export class Store {
|
|
|
296
283
|
if (this.#남이썼나()) { this.#못썼다({ code: 'OTHER_WINDOW' }); return false; }
|
|
297
284
|
try {
|
|
298
285
|
if (!this.#줄끝봤나) {
|
|
299
|
-
if (
|
|
286
|
+
if (!줄로끝나나(this.file)) 줄 = '\n' + 줄;
|
|
300
287
|
this.#줄끝봤나 = true;
|
|
301
288
|
}
|
|
302
289
|
appendFileSync(this.file, 줄, 'utf8');
|
package/src/backend/ctxsize.js
CHANGED
|
@@ -97,7 +97,7 @@ function 성한수(v) {
|
|
|
97
97
|
*
|
|
98
98
|
* llama.cpp 의 /props 나 일부 게이트웨이의 설명 필드도 같은 모양이다.
|
|
99
99
|
*/
|
|
100
|
-
|
|
100
|
+
function 글에서찾기(s, 이름들) {
|
|
101
101
|
const 글 = String(s ?? '');
|
|
102
102
|
if (!글 || 글.length > 200000) return null;
|
|
103
103
|
for (const 이름 of 이름들) {
|
|
@@ -122,7 +122,7 @@ export function 글에서찾기(s, 이름들) {
|
|
|
122
122
|
* 어느 이름에서 찾았는지도 같이 돌려준다 — 값이 이상할 때 사람이 원인을
|
|
123
123
|
* 짚을 수 있어야 한다. /ctx 자세히 가 이걸 보여 준다.
|
|
124
124
|
*/
|
|
125
|
-
|
|
125
|
+
function 파보기(obj, 이름들, { 글도 = true, depth = 0 } = {}) {
|
|
126
126
|
if (!obj || typeof obj !== 'object' || depth > 5) return null;
|
|
127
127
|
for (const key of 이름들) {
|
|
128
128
|
const v = 성한수(obj[key]);
|
package/src/backend/http.js
CHANGED
|
@@ -304,7 +304,7 @@ function 신호(timeout, signal) {
|
|
|
304
304
|
*/
|
|
305
305
|
export const 잠잠기본 = 60000;
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
function 멎음오류(잠잠) {
|
|
308
308
|
return Object.assign(
|
|
309
309
|
new Error(`${초로(잠잠)}초 동안 아무것도 안 왔습니다 — 흐름이 멎었습니다`),
|
|
310
310
|
{ name: 'StallError', code: 'STALL', 잠잠 },
|
package/src/backend/learn.js
CHANGED
|
@@ -29,6 +29,32 @@ const 성한수 = (v) => {
|
|
|
29
29
|
return Number.isFinite(n) && n >= 최소 && n <= 최대허용 ? n : null;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
/*
|
|
33
|
+
* 창 한계 문장에서 **우리가 보낸 크기**를 뽑는다 (2.0.2 · 488).
|
|
34
|
+
*
|
|
35
|
+
* 한계 뒤 `requested N` 만 봤다. 요즘 vLLM 은 이렇게 말한다 —
|
|
36
|
+
*
|
|
37
|
+
* However, your request has 40000 input tokens. → 못 뽑음 (null)
|
|
38
|
+
* However, you requested 1024 output tokens and your prompt contains at least
|
|
39
|
+
* 7169 input tokens, for a total of at least 8193 tokens. → 1024 로 뽑음
|
|
40
|
+
*
|
|
41
|
+
* 뒤엣것은 틀린 수다 — 화면이 「(1,024 을 보냈었습니다)」 라고, 8,192 창에 1,024 를 보내 거절당한
|
|
42
|
+
* 것처럼 말한다. 합이 있으면 합, 없으면 답 길이가 아닌 `requested N`, 그다음 입력 크기 순으로 본다.
|
|
43
|
+
*/
|
|
44
|
+
function 보낸크기(s) {
|
|
45
|
+
const 차례 = [
|
|
46
|
+
/for a total of\s+(?:at least\s+)?(\d+)/i,
|
|
47
|
+
/requested\s+(\d+)(?!\d)(?!\s*(?:output|completion|new)\b)/i,
|
|
48
|
+
/requested tokens\s*\(\s*(\d+)\s*\)/i,
|
|
49
|
+
/(?:request has|prompt contains|input (?:has|contains))\s+(?:at least\s+)?(\d+)/i,
|
|
50
|
+
];
|
|
51
|
+
for (const re of 차례) {
|
|
52
|
+
const n = 성한수(re.exec(s)?.[1]);
|
|
53
|
+
if (n) return n;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
/**
|
|
33
59
|
* 거절 문장에서 배울 것이 있나.
|
|
34
60
|
*
|
|
@@ -107,7 +133,7 @@ export function 배울것(message) {
|
|
|
107
133
|
if (!m) continue;
|
|
108
134
|
const limit = 성한수(m[1]);
|
|
109
135
|
if (!limit) continue;
|
|
110
|
-
return { kind: 'ctx', limit, asked:
|
|
136
|
+
return { kind: 'ctx', limit, asked: 보낸크기(s), text: 짧게(원문) };
|
|
111
137
|
}
|
|
112
138
|
|
|
113
139
|
// ── 2) 답 길이 한계 ───────────────────────────────────────────────────
|