deel-local-cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +347 -9
- package/README.md +317 -6
- package/package.json +2 -2
- package/src/agent/budget.js +0 -14
- package/src/agent/compact.js +87 -0
- package/src/agent/evolve.js +213 -0
- package/src/agent/loop.js +38 -1
- package/src/agent/modes.js +2 -0
- package/src/agent/route.js +55 -3
- package/src/agent/session.js +145 -1
- package/src/agent/threads.js +154 -0
- package/src/backend/http.js +41 -4
- package/src/commands.js +268 -4
- package/src/config.js +10 -1
- package/src/oneshot.js +4 -0
- package/src/preview/serve.js +326 -0
- package/src/repl.js +210 -34
- package/src/safety/guard.js +37 -2
- package/src/safety/undo.js +1 -1
- package/src/setup.js +2 -2
- package/src/skills/builtin//352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md +64 -0
- package/src/skills/builtin//352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md +78 -0
- package/src/skills/builtin//353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md +65 -0
- package/src/skills/builtin//354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md +74 -0
- package/src/skills/builtin//354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md +59 -0
- package/src/skills/builtin//354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md +73 -0
- package/src/skills/builtin//354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md +66 -0
- package/src/skills/discover.js +18 -3
- package/src/tools/edit-match.js +16 -5
- package/src/tools/excel-com.js +1 -1
- package/src/tools/excel.js +1 -1
- package/src/tools/fsutil.js +0 -8
- package/src/tools/index.js +1 -1
- package/src/tools/todo.js +19 -3
- package/src/ui/ansi.js +6 -2
- package/src/ui/inputbox.js +37 -8
- package/src/ui/md.js +227 -0
- package/src/ui/motion.js +212 -0
- package/src/ui/prompt.js +1 -1
- package/src/ui/screen.js +1 -1
- package/src/ui/status.js +14 -1
- package/src/ui/working.js +0 -3
- package/src/ui/wrap.js +1 -8
package/src/agent/compact.js
CHANGED
|
@@ -15,6 +15,93 @@ export const COMPACT_AT = 0.8;
|
|
|
15
15
|
export const KEEP_TAIL_RATIO = 0.3; // 최근 대화 중 남길 비율
|
|
16
16
|
export const KEEP_HEAD = 2; // 맨 처음 요청은 언제나 남긴다 (목표가 거기 있다)
|
|
17
17
|
|
|
18
|
+
/*
|
|
19
|
+
* ── 그 전에, 도구 결과부터 접는다 ──────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* 요약 압축은 잃는 것이 크다. 사람이 한 말도, 모델이 왜 그렇게 정했는지도
|
|
22
|
+
* 세 줄로 줄어든다. 그런데 자리를 실제로 먹고 있는 것은 대개 그쪽이 아니다.
|
|
23
|
+
*
|
|
24
|
+
* Read 로 600줄짜리 파일을 한 번 열면 32k 창의 15% 가 그 자리에서 사라지고,
|
|
25
|
+
* 그 사본은 대화가 끝날 때까지 이력에 그대로 눌러앉는다. 이미 다 읽고 고친
|
|
26
|
+
* 파일인데도 그렇다. 세 번 읽으면 창의 절반이 옛날 파일 내용이다.
|
|
27
|
+
*
|
|
28
|
+
* 그래서 80% 에서 통째로 요약하기 전에, 먼저 **오래된 도구 결과만** 한 줄로
|
|
29
|
+
* 접는다. 사람 말과 모델의 판단은 한 글자도 안 건드린다. 무엇을 읽었는지는
|
|
30
|
+
* 남기므로 모델은 필요하면 그 파일을 다시 읽으면 된다 — 읽는 값은 싸고,
|
|
31
|
+
* 하던 일을 잊는 값은 비싸다.
|
|
32
|
+
*
|
|
33
|
+
* 최근 것은 안 접는다. 방금 읽은 것을 접으면 그 자리에서 다시 읽게 되고,
|
|
34
|
+
* 그러면 접은 보람이 없다.
|
|
35
|
+
*/
|
|
36
|
+
export const FOLD_AT = 0.55; // 이 아래로는 접을 이유가 없다
|
|
37
|
+
export const KEEP_RECENT = 4; // 최근 도구 결과 이만큼은 원문 그대로 둔다
|
|
38
|
+
export const FOLD_MIN = 300; // 이보다 작으면 접어도 자리가 안 준다 (글자 수)
|
|
39
|
+
export const 접힘표 = '(접힘)';
|
|
40
|
+
|
|
41
|
+
/** 인자에서 사람이 알아볼 한 조각만 뽑는다. 경로가 제일 쓸모 있다. */
|
|
42
|
+
function 어디(args) {
|
|
43
|
+
if (!args || typeof args !== 'object') return '';
|
|
44
|
+
for (const k of ['file_path', 'path', 'pattern', 'command', 'query', 'url', '목적']) {
|
|
45
|
+
const v = args[k];
|
|
46
|
+
if (typeof v === 'string' && v.trim()) return v.trim().replace(/\s+/g, ' ').slice(0, 60);
|
|
47
|
+
}
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function shouldFold(session, at = FOLD_AT) {
|
|
52
|
+
const b = session.breakdown();
|
|
53
|
+
return b.total > 0 && b.used / b.total >= at;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 오래된 도구 결과를 한 줄로 접는다.
|
|
58
|
+
*
|
|
59
|
+
* 짝은 안 건드린다 — 메시지를 지우는 게 아니라 **내용만** 바꾸므로 호출과
|
|
60
|
+
* 결과의 짝이 그대로다. 규격이 깨질 자리가 아예 없다.
|
|
61
|
+
*
|
|
62
|
+
* @returns {{접은것: number, 아낀토큰: number}}
|
|
63
|
+
*/
|
|
64
|
+
export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN } = {}) {
|
|
65
|
+
const ms = session.messages ?? [];
|
|
66
|
+
|
|
67
|
+
// 호출 쪽에서 이름과 인자를 가져온다. 결과 메시지에는 그게 안 실려 있다.
|
|
68
|
+
const 이름표 = new Map();
|
|
69
|
+
for (const m of ms) {
|
|
70
|
+
for (const c of m?.tool_calls ?? []) {
|
|
71
|
+
let args = {};
|
|
72
|
+
try { args = JSON.parse(c?.function?.arguments ?? '{}'); } catch { /* 못 읽으면 그만 */ }
|
|
73
|
+
if (c?.id) 이름표.set(c.id, { name: c?.function?.name ?? '도구', args });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const 자리 = [];
|
|
78
|
+
ms.forEach((m, i) => {
|
|
79
|
+
if (m?.role !== 'tool') return;
|
|
80
|
+
const 글 = typeof m.content === 'string' ? m.content : '';
|
|
81
|
+
if (글.startsWith(접힘표)) return; // 이미 접은 것
|
|
82
|
+
자리.push({ i, 글 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const 접을것 = 자리.slice(0, Math.max(0, 자리.length - keep)).filter((x) => x.글.length >= min);
|
|
86
|
+
let 아낀토큰 = 0;
|
|
87
|
+
|
|
88
|
+
for (const { i, 글 } of 접을것) {
|
|
89
|
+
const m = ms[i];
|
|
90
|
+
const 아는것 = 이름표.get(m.tool_call_id) ?? { name: m.tool_name ?? '도구', args: {} };
|
|
91
|
+
const 곳 = 어디(아는것.args);
|
|
92
|
+
const 줄수 = 글.split('\n').length;
|
|
93
|
+
const 전 = estimateTokens(글);
|
|
94
|
+
ms[i] = {
|
|
95
|
+
...m,
|
|
96
|
+
content: `${접힘표} ${아는것.name}${곳 ? `(${곳})` : ''} — ${줄수}줄. `
|
|
97
|
+
+ '자리를 비우려고 내용을 접었습니다. 필요하면 다시 읽으세요.',
|
|
98
|
+
};
|
|
99
|
+
아낀토큰 += 전 - estimateTokens(ms[i].content);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { 접은것: 접을것.length, 아낀토큰: Math.max(0, 아낀토큰) };
|
|
103
|
+
}
|
|
104
|
+
|
|
18
105
|
const 요약지시 = `지금까지의 대화를 다음 형식으로 요약하세요. 이어서 일할 사람이 이것만 보고도 계속할 수 있어야 합니다.
|
|
19
106
|
추측하지 말고 실제로 오간 내용만 쓰세요. 한국어로, 각 항목 3줄 이내로 쓰세요.
|
|
20
107
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// 쓸수록 이 PC 에 맞춰 나아진다.
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 만드나 ───────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// deel 은 이미 세 군데서 뭔가를 배우고 있었다. 그런데 셋 다 **끄면 사라졌다.**
|
|
6
|
+
//
|
|
7
|
+
// backend/learn.js 서버가 거절하면 그 말에서 상한을 배운다 → 그 세션에서만
|
|
8
|
+
// agent/grade.js 모델이 인자를 자주 잘라 먹는지 지켜본다 → 그 세션에서만
|
|
9
|
+
// session.배운다() 토큰 추정을 실제값에 맞춘다 → 그 세션에서만
|
|
10
|
+
//
|
|
11
|
+
// 그래서 켤 때마다 처음부터 다시 겪는다. 어제 `pnpm` 이 이 PC 에 없다는 걸
|
|
12
|
+
// 알아냈어도 오늘 또 부르고, 또 실패하고, 또 우회한다. 같은 실수를 매일 한다.
|
|
13
|
+
//
|
|
14
|
+
// 여기서는 **겪은 것을 조금씩 쌓아 두고**, 다음에 켤 때 몇 줄로 요약해 넘긴다.
|
|
15
|
+
//
|
|
16
|
+
// ── 학습이 아니다 ───────────────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// 모델을 고치는 것이 아니고, 대화를 통째로 쌓아 두는 것도 아니다. 그러면
|
|
19
|
+
// 컨텍스트만 먹는다. 여기 쌓는 것은 **확인된 사실 몇 줄**이다.
|
|
20
|
+
//
|
|
21
|
+
// · 세어 두기만 한다 — 무엇이 몇 번 됐고 몇 번 안 됐나
|
|
22
|
+
// · 확신이 설 때만 말한다 — 한 번 겪은 것은 우연일 수 있다
|
|
23
|
+
// · 상한을 못 박는다 — 프롬프트에 실리는 것은 220토큰까지다
|
|
24
|
+
//
|
|
25
|
+
// 자리를 아끼는 쪽이 오히려 값이 크다. 여기 한 줄이 모델이 헛도는 걸음
|
|
26
|
+
// 서너 개를 없앤다 — 그 걸음 하나가 파일 내용 한 벌씩이다.
|
|
27
|
+
//
|
|
28
|
+
// ── 무엇을 어디에 쌓나 ──────────────────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// <작업폴더>/.deel/배운것.json 이 프로젝트에서만 맞는 것 (명령·인코딩)
|
|
31
|
+
// <설정폴더>/배운것.json 이 PC·이 모델에 붙는 것 (버릇·토큰 보정)
|
|
32
|
+
//
|
|
33
|
+
// 나눠 두는 이유는 하나다. "`npm test` 가 된다" 는 이 폴더의 사실이고,
|
|
34
|
+
// "이 모델은 인자를 잘라 먹는다" 는 모델의 사실이다. 섞어 두면 폴더를 옮길
|
|
35
|
+
// 때마다 모델에 대해 알아낸 것을 통째로 잃는다.
|
|
36
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
37
|
+
import { join, dirname } from 'node:path';
|
|
38
|
+
import { estimateTokens } from './session.js';
|
|
39
|
+
|
|
40
|
+
export const 최대토큰 = 220; // 프롬프트에 실을 상한
|
|
41
|
+
const 명령최대 = 40; // 표가 끝없이 자라지 않게
|
|
42
|
+
const 모델최대 = 10;
|
|
43
|
+
|
|
44
|
+
const 빈것 = () => ({ 판: 1, 명령: {}, 모델: {} });
|
|
45
|
+
|
|
46
|
+
function 읽기(파일) {
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(파일)) return 빈것();
|
|
49
|
+
const j = JSON.parse(readFileSync(파일, 'utf8'));
|
|
50
|
+
return { ...빈것(), ...(j && typeof j === 'object' ? j : {}) };
|
|
51
|
+
} catch { return 빈것(); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function 쓰기(파일, 값) {
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(dirname(파일), { recursive: true });
|
|
57
|
+
writeFileSync(파일, JSON.stringify(값, null, 2), 'utf8');
|
|
58
|
+
} catch { /* 못 적어도 대화는 계속돼야 한다 */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 오래된 것부터 버려서 표를 상한 안에 둔다. */
|
|
62
|
+
function 줄이기(표, 상한) {
|
|
63
|
+
const 열쇠 = Object.keys(표);
|
|
64
|
+
if (열쇠.length <= 상한) return 표;
|
|
65
|
+
열쇠.sort((a, b) => String(표[a]?.at ?? '').localeCompare(String(표[b]?.at ?? '')));
|
|
66
|
+
for (const k of 열쇠.slice(0, 열쇠.length - 상한)) delete 표[k];
|
|
67
|
+
return 표;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class 배움 {
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} root 작업 폴더
|
|
73
|
+
* @param {string} home 설정 폴더 (~/.deel 또는 DEEL_HOME)
|
|
74
|
+
* @param {string} 지금 시각. 검사가 고정할 수 있게 밖에서 받는다
|
|
75
|
+
*/
|
|
76
|
+
constructor(root, home, 지금 = new Date().toISOString()) {
|
|
77
|
+
this.지금 = 지금;
|
|
78
|
+
this.폴더파일 = root ? join(root, '.deel', '배운것.json') : null;
|
|
79
|
+
this.집파일 = home ? join(home, '배운것.json') : null;
|
|
80
|
+
this.폴더 = this.폴더파일 ? 읽기(this.폴더파일) : 빈것();
|
|
81
|
+
this.집 = this.집파일 ? 읽기(this.집파일) : 빈것();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 명령 하나가 됐는지 안 됐는지.
|
|
86
|
+
*
|
|
87
|
+
* 명령줄 전체가 아니라 **앞머리 두 낱말**만 센다. `npm test -- --watch` 와
|
|
88
|
+
* `npm test` 는 같은 사실이고, 경로·인자까지 세면 표가 금세 쓸모없어진다.
|
|
89
|
+
*
|
|
90
|
+
* 다만 **프로그램이 아예 없어서** 실패한 것은 첫 낱말만 센다. `pnpm install`
|
|
91
|
+
* 이 안 되고 `pnpm add` 도 안 됐다면 그건 두 가지 사실이 아니라 "이 PC 에
|
|
92
|
+
* pnpm 이 없다" 는 한 가지 사실이다. 두 낱말로 세면 서로 다른 칸에 하나씩
|
|
93
|
+
* 쌓여서 영영 확신에 못 이른다 — 매번 다시 겪는다.
|
|
94
|
+
*/
|
|
95
|
+
명령본것(명령, 됐나, 이유 = '') {
|
|
96
|
+
const 낱말 = String(명령 ?? '').trim().split(/\s+/);
|
|
97
|
+
const 없는프로그램 = !됐나
|
|
98
|
+
&& /not found|not recognized|ENOENT|찾을 수 없|없는 명령/i.test(String(이유 ?? ''));
|
|
99
|
+
const 열쇠 = (없는프로그램 ? 낱말.slice(0, 1) : 낱말.slice(0, 2)).join(' ').slice(0, 40);
|
|
100
|
+
if (!열쇠) return this;
|
|
101
|
+
const 표 = this.폴더.명령;
|
|
102
|
+
const r = 표[열쇠] ?? { ok: 0, no: 0, at: this.지금 };
|
|
103
|
+
if (됐나) r.ok++; else r.no++;
|
|
104
|
+
r.at = this.지금;
|
|
105
|
+
표[열쇠] = r;
|
|
106
|
+
줄이기(표, 명령최대);
|
|
107
|
+
this.#저장폴더();
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 모델이 보인 버릇 하나. grade.js 의 지켜본것 과 같은 이름을 쓴다. */
|
|
112
|
+
모델본것(모델, 무엇, n = 1) {
|
|
113
|
+
const r = this.#모델칸(모델);
|
|
114
|
+
if (!r) return this;
|
|
115
|
+
r[무엇] = (r[무엇] ?? 0) + n;
|
|
116
|
+
r.at = this.지금;
|
|
117
|
+
this.#저장집();
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 토큰 추정 배수. 다음에 켤 때 이 값으로 시작한다. */
|
|
122
|
+
보정본것(모델, 배수) {
|
|
123
|
+
const n = Number(배수);
|
|
124
|
+
if (!Number.isFinite(n) || n < 0.5 || n > 2) return this;
|
|
125
|
+
const r = this.#모델칸(모델);
|
|
126
|
+
if (!r) return this;
|
|
127
|
+
// 갑자기 튀지 않게 지난 값과 섞는다. 대화마다 조금씩 옮겨 간다.
|
|
128
|
+
r.보정 = r.보정 ? r.보정 + (n - r.보정) * 0.5 : n;
|
|
129
|
+
r.at = this.지금;
|
|
130
|
+
this.#저장집();
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 지난번에 알아낸 토큰 배수. 없으면 null. */
|
|
135
|
+
아는보정(모델) {
|
|
136
|
+
const r = this.집.모델?.[String(모델 ?? '')];
|
|
137
|
+
return r?.보정 && r.보정 >= 0.5 && r.보정 <= 2 ? r.보정 : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#모델칸(모델) {
|
|
141
|
+
const 이름 = String(모델 ?? '').trim();
|
|
142
|
+
if (!이름) return null;
|
|
143
|
+
const 표 = this.집.모델;
|
|
144
|
+
표[이름] = 표[이름] ?? { 걸음: 0, at: this.지금 };
|
|
145
|
+
줄이기(표, 모델최대);
|
|
146
|
+
return 표[이름];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#저장폴더() { if (this.폴더파일) 쓰기(this.폴더파일, this.폴더); }
|
|
150
|
+
#저장집() { if (this.집파일) 쓰기(this.집파일, this.집); }
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 프롬프트에 실을 몇 줄.
|
|
154
|
+
*
|
|
155
|
+
* 확신이 서는 것만 말한다. 두 번 겪지 않은 것은 우연일 수 있고, 우연을
|
|
156
|
+
* 사실처럼 적으면 모델이 그 위에서 엉뚱한 판단을 한다 — 안 배우느니만 못하다.
|
|
157
|
+
*
|
|
158
|
+
* @returns {string|null} 없으면 null (그때는 프롬프트에 아무것도 안 붙는다)
|
|
159
|
+
*/
|
|
160
|
+
요약(모델 = '', 상한 = 최대토큰) {
|
|
161
|
+
const 줄 = [];
|
|
162
|
+
|
|
163
|
+
// 되는 명령 / 안 되는 명령. 이 폴더의 사실이다.
|
|
164
|
+
const 되는것 = [];
|
|
165
|
+
const 안되는것 = [];
|
|
166
|
+
for (const [열쇠, r] of Object.entries(this.폴더.명령 ?? {})) {
|
|
167
|
+
if (r.ok >= 2 && r.no === 0) 되는것.push(열쇠);
|
|
168
|
+
else if (r.no >= 2 && r.ok === 0) 안되는것.push(열쇠);
|
|
169
|
+
}
|
|
170
|
+
if (되는것.length) 줄.push(`- 여기서 되는 명령: ${되는것.slice(0, 6).map((x) => `\`${x}\``).join(' · ')}`);
|
|
171
|
+
if (안되는것.length) 줄.push(`- 이 PC 에서 안 되는 명령(다시 부르지 마라): ${안되는것.slice(0, 6).map((x) => `\`${x}\``).join(' · ')}`);
|
|
172
|
+
|
|
173
|
+
// 모델 버릇. 걸음이 쌓여야 말이 된다.
|
|
174
|
+
const m = this.집.모델?.[String(모델 ?? '')];
|
|
175
|
+
if (m && (m.걸음 ?? 0) >= 10) {
|
|
176
|
+
if ((m.잘린인자 ?? 0) / m.걸음 >= 0.15) {
|
|
177
|
+
줄.push('- 이 모델은 큰 인자를 자주 잘라 먹었다. 파일은 Write 한 번에 다 쓰지 말고 Append 로 나눠 써라.');
|
|
178
|
+
}
|
|
179
|
+
if ((m.편집실패 ?? 0) / m.걸음 >= 0.15) {
|
|
180
|
+
줄.push('- Edit 이 자주 빗나갔다. old_string 은 Read 로 본 그대로, 공백까지 옮겨 담아라.');
|
|
181
|
+
}
|
|
182
|
+
if ((m.되풀이 ?? 0) / m.걸음 >= 0.15) {
|
|
183
|
+
줄.push('- 같은 호출을 되풀이한 적이 잦다. 한 번 실패한 방법은 바꿔서 시도해라.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!줄.length) return null;
|
|
188
|
+
|
|
189
|
+
const 머리 = '## 여기서 겪어 본 것 (자동으로 쌓임)';
|
|
190
|
+
const out = [머리];
|
|
191
|
+
for (const l of 줄) {
|
|
192
|
+
if (estimateTokens([...out, l].join('\n')) > 상한) break;
|
|
193
|
+
out.push(l);
|
|
194
|
+
}
|
|
195
|
+
return out.length > 1 ? out.join('\n') : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 사람이 지우라고 한 것. 파일째 비운다. */
|
|
199
|
+
지우기(어디 = '전부') {
|
|
200
|
+
if (어디 === '전부' || 어디 === '폴더') { this.폴더 = 빈것(); this.#저장폴더(); }
|
|
201
|
+
if (어디 === '전부' || 어디 === '모델') { this.집 = 빈것(); this.#저장집(); }
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 화면에 뿌릴 것. */
|
|
206
|
+
현황(모델 = '') {
|
|
207
|
+
const 명령 = Object.entries(this.폴더.명령 ?? {})
|
|
208
|
+
.map(([이름, r]) => ({ 이름, ...r }))
|
|
209
|
+
.sort((a, b) => (b.ok + b.no) - (a.ok + a.no));
|
|
210
|
+
const m = this.집.모델?.[String(모델 ?? '')] ?? null;
|
|
211
|
+
return { 명령, 모델: m, 모델이름: String(모델 ?? '') };
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/agent/loop.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isMutating } from '../safety/guard.js';
|
|
|
6
6
|
import { effortFor, tokensFor, fullCap, wasCut, shiftLevel } from './effort.js';
|
|
7
7
|
import { 살린쓰기 } from './salvage.js';
|
|
8
8
|
import { 배울것, 길이문제인가 } from '../backend/learn.js';
|
|
9
|
-
import { compact, shouldCompact } from './compact.js';
|
|
9
|
+
import { compact, shouldCompact, shouldFold, foldToolResults } from './compact.js';
|
|
10
10
|
import { 걸음수, 하위걸음수, 요약길이 } from './budget.js';
|
|
11
11
|
import { Session } from './session.js';
|
|
12
12
|
import { 최대깊이, 하위모드, 하위요약 } from '../tools/task.js';
|
|
@@ -240,6 +240,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
240
240
|
* 세기만 한다 — 판단은 grade.js 가 하고, 프롬프트는 다음 걸음부터 달라진다.
|
|
241
241
|
*/
|
|
242
242
|
session.본것?.걸음셈();
|
|
243
|
+
ctx.배움?.모델본것(conn.model, '걸음');
|
|
243
244
|
// 단계마다 필요한 생각의 양이 다르다. effort.js 가 그 배분을 갖고 있다.
|
|
244
245
|
const stage = steps === 1 ? 'plan' : lastToolFailed ? 'fix' : 'work';
|
|
245
246
|
const level = effortFor(think, effort, stage);
|
|
@@ -332,6 +333,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
332
333
|
}
|
|
333
334
|
if (빈답인가(msg)) {
|
|
334
335
|
session.본것?.본것('빈답');
|
|
336
|
+
ctx.배움?.모델본것(conn.model, '빈답');
|
|
335
337
|
yield {
|
|
336
338
|
type: 'error',
|
|
337
339
|
text: '서버가 빈 답을 보냈습니다.\n'
|
|
@@ -403,6 +405,16 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
403
405
|
session.usage.out += msg.usage?.out ?? 0;
|
|
404
406
|
session.usage.calls++;
|
|
405
407
|
|
|
408
|
+
/*
|
|
409
|
+
* 방금 보낸 것이 실제로 몇 토큰이었는지 서버가 알려 줬다. 우리 추정과
|
|
410
|
+
* 견줘서 배운다 — 여기가 유일하게 정답을 아는 자리다.
|
|
411
|
+
*
|
|
412
|
+
* 답을 push 하기 **전**이어야 한다. 지금 이력이 곧 방금 보낸 프롬프트다.
|
|
413
|
+
*/
|
|
414
|
+
const 새보정 = session.배운다?.(msg.usage?.in);
|
|
415
|
+
// 배운 배수를 디스크에도 남긴다. 다음에 켤 때 이 값으로 시작한다.
|
|
416
|
+
if (새보정) ctx.배움?.보정본것(conn.model, 새보정);
|
|
417
|
+
|
|
406
418
|
session.push(assistantMessage(conn.kind, msg));
|
|
407
419
|
|
|
408
420
|
if (!msg.toolCalls?.length) {
|
|
@@ -438,6 +450,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
438
450
|
// 모델은 고칠 게 없다고 보고 똑같이 다시 시도한다. 그래서 끝없이 돈다.
|
|
439
451
|
if (call.argsBroken) {
|
|
440
452
|
session.본것?.본것('잘린인자');
|
|
453
|
+
ctx.배움?.모델본것(conn.model, '잘린인자');
|
|
441
454
|
// ── 버리기 전에, 건질 수 있는지 먼저 본다 ──────────────────────
|
|
442
455
|
//
|
|
443
456
|
// 잘린 JSON 안에는 이미 받아 놓은 내용이 들어 있다. 경로도 대개 온전하다.
|
|
@@ -704,6 +717,18 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
704
717
|
if (call.name === 'Edit' && result.error) session.본것?.본것('편집실패');
|
|
705
718
|
else if (!result.error) session.본것?.본것('도구성공');
|
|
706
719
|
|
|
720
|
+
/*
|
|
721
|
+
* 겪은 것을 디스크에도 쌓는다 (agent/evolve.js).
|
|
722
|
+
*
|
|
723
|
+
* 위의 지켜본것은 이 대화가 끝나면 사라진다. 그래서 어제 알아낸 것을
|
|
724
|
+
* 오늘 또 알아내야 했다 — `pnpm` 이 이 PC 에 없다는 걸 매일 다시 겪는다.
|
|
725
|
+
* 명령의 성패는 이 폴더의 사실이므로 폴더 쪽에 남긴다.
|
|
726
|
+
*/
|
|
727
|
+
if (call.name === 'Bash' && call.args?.command) {
|
|
728
|
+
ctx.배움?.명령본것(call.args.command, !result.error, result.error ?? '');
|
|
729
|
+
}
|
|
730
|
+
if (call.name === 'Edit' && result.error) ctx.배움?.모델본것(conn.model, '편집실패');
|
|
731
|
+
|
|
707
732
|
if (result.error) {
|
|
708
733
|
lastToolFailed = true;
|
|
709
734
|
// 같은 도구가 같은 이유로 계속 실패하면 헛돌고 있는 것이다.
|
|
@@ -755,6 +780,18 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
755
780
|
return;
|
|
756
781
|
}
|
|
757
782
|
|
|
783
|
+
/*
|
|
784
|
+
* 요약해서 접기 **전에** 도구 결과부터 접는다.
|
|
785
|
+
*
|
|
786
|
+
* 자리를 먹는 것은 대개 사람 말이 아니라 옛날에 읽어 둔 파일이다. 그쪽을
|
|
787
|
+
* 먼저 비우면 요약 압축을 한참 미룰 수 있고, 미루는 동안 대화는 한 글자도
|
|
788
|
+
* 안 잃는다. 그래도 차면 아래에서 통째로 요약한다.
|
|
789
|
+
*/
|
|
790
|
+
if (shouldFold(session)) {
|
|
791
|
+
const f = foldToolResults(session);
|
|
792
|
+
if (f.접은것) yield { type: 'folded', ...f };
|
|
793
|
+
}
|
|
794
|
+
|
|
758
795
|
// 컨텍스트가 차오르면 오래된 대화를 '요약해서' 접는다. 그냥 자르면 하던 일을 잊는다.
|
|
759
796
|
if (shouldCompact(session)) {
|
|
760
797
|
yield { type: 'compacting' };
|
package/src/agent/modes.js
CHANGED
|
@@ -233,6 +233,8 @@ export const MODES = {
|
|
|
233
233
|
" 6. 확인 방법 — 무엇을 돌려 보면 됐는지 알 수 있는가",
|
|
234
234
|
"",
|
|
235
235
|
"단계는 TodoWrite 로도 적어라. 승인 뒤 그대로 이어서 하게 된다.",
|
|
236
|
+
" 단계 수는 정해져 있지 않다 — 일의 크기에 맞춰라. 세 개로 맞추지 마라.",
|
|
237
|
+
" 작은 일이면 두세 개로 끝내고, 큰 일이면 열 개가 넘어도 그대로 다 적어라.",
|
|
236
238
|
"모르는 것이 있으면 지어내지 말고 \"이건 확인이 필요하다\" 고 적어라.",
|
|
237
239
|
"",
|
|
238
240
|
"마지막에 \"이대로 진행할까요?\" 로 끝내라. 승인을 받으면 /code 로 바꿔 실행한다.",
|
package/src/agent/route.js
CHANGED
|
@@ -110,15 +110,66 @@ const 문턱 = {
|
|
|
110
110
|
ask: 5,
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
/*
|
|
114
|
+
* ── 겹친 요청 ───────────────────────────────────────────────────────────
|
|
115
|
+
*
|
|
116
|
+
* "계획해주고 만들어줘" 처럼 **계획과 실행이 한 말에 같이** 든 것.
|
|
117
|
+
*
|
|
118
|
+
* 이건 위의 점수표로는 못 고른다. '만들어' 가 code 3점 문턱을 그냥 넘어서
|
|
119
|
+
* 곧장 코드 모드로 가 버리고, 사람이 보자고 한 계획은 안 나온다.
|
|
120
|
+
* 실제로 겪었다 — "정리해서 만들어줘" 가 계획 한 줄 없이 파일부터 만들었다.
|
|
121
|
+
*
|
|
122
|
+
* 반대로 plan 으로 보내면 계획만 내고 멈춘다. 그것도 시킨 일의 절반이다.
|
|
123
|
+
* 그래서 따로 알아본다. 계획을 내고 → 승인을 받고 → 그대로 실행까지 잇는다.
|
|
124
|
+
*
|
|
125
|
+
* 좁게 잡는다. 사용자는 자율 실행을 원했고, 승인 창은 그 결정을 갉아먹는
|
|
126
|
+
* 쪽이다. '고쳐줘' 한마디에 창이 뜨면 그게 더 나쁘다.
|
|
127
|
+
* 그래서 **계획말 + 잇는말** 이 앞에 있고 그 **뒤에** 실행말이 와야만 겹침이다.
|
|
128
|
+
*/
|
|
129
|
+
const 계획말 = '계획|플랜|설계|구상|기획|정리|검토|조사|파악|살펴|알아보|분석|로드맵|방향|순서|밑그림|초안';
|
|
130
|
+
// '정리하고' 처럼 잇는 꼴이어야 한다. 그냥 '정리' 하나로는 겹침이 아니다 —
|
|
131
|
+
// "코드 정리해줘" 는 계획이 아니라 그 자체가 할 일이다.
|
|
132
|
+
const 잇는말 = '해\\s*주고|해\\s*놓고|하고\\s*나서|하고서|한\\s*다음|한\\s*뒤|한\\s*후|하고|해서|잡고|짜고|세우고|낸\\s*뒤|낸\\s*다음';
|
|
133
|
+
const 실행말 = /만들어|만들자|만들어라|구현|작성|추가(해|하)|생성|써\s*(줘|주세요)|짜\s*(줘|주세요)|고쳐|세팅|설치|배포/;
|
|
134
|
+
const 계획먼저 = new RegExp(`(${계획말})\\s*(?:을|를)?\\s*(?:${잇는말})`);
|
|
135
|
+
// 영어도 같이 본다 — "plan and then build it" 처럼 붙여 넣는 일이 있다.
|
|
136
|
+
const 영어겹침 = /\b(plan|design|outline|draft)\b[\s\S]{0,60}?\b(?:and|then|,)\s*(?:then\s*)?\b(build|implement|create|write|make|code)\b/i;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 계획과 실행이 한 말에 같이 들었나.
|
|
140
|
+
*
|
|
141
|
+
* @returns {{겹침: boolean, why: string}} 겹침이 아니면 why 는 빈 글.
|
|
142
|
+
*/
|
|
143
|
+
export function 겹친요청(text) {
|
|
144
|
+
const s = String(text ?? '');
|
|
145
|
+
const 영어 = s.match(영어겹침);
|
|
146
|
+
if (영어) return { 겹침: true, why: `'${영어[1]}' 과 '${영어[2]}'` };
|
|
147
|
+
|
|
148
|
+
const 앞 = s.match(계획먼저);
|
|
149
|
+
if (!앞) return { 겹침: false, why: '' };
|
|
150
|
+
// 실행말이 계획말 **뒤에** 와야 한다. "만들고 나서 정리해줘" 는 겹침이 아니다 —
|
|
151
|
+
// 그건 만드는 일이 먼저고, 계획을 미리 볼 것이 없다.
|
|
152
|
+
const 뒤 = s.slice(앞.index + 앞[0].length).match(실행말);
|
|
153
|
+
if (!뒤) return { 겹침: false, why: '' };
|
|
154
|
+
return { 겹침: true, why: `'${앞[0].trim()}' 뒤에 '${뒤[0].trim()}'` };
|
|
155
|
+
}
|
|
156
|
+
|
|
113
157
|
/**
|
|
114
158
|
* 이 한마디가 무슨 일인지 고른다.
|
|
115
159
|
*
|
|
116
160
|
* @param {string} text 사용자가 친 말
|
|
117
|
-
* @returns {{ mode: string|null, score: number, why: string, 점수들: object }}
|
|
161
|
+
* @returns {{ mode: string|null, score: number, why: string, 겹침: boolean, 점수들: object }}
|
|
118
162
|
* mode 가 null 이면 '모르겠다' 는 뜻이다 — 종합 모드에 그대로 둔다.
|
|
163
|
+
* 겹침이 true 면 계획을 내고 승인을 받아 실행까지 이어야 한다는 뜻이다.
|
|
119
164
|
*/
|
|
120
165
|
export function route(text) {
|
|
121
166
|
const s = String(text ?? '');
|
|
167
|
+
|
|
168
|
+
// 겹친 요청은 점수표보다 먼저 본다. 점수표에 맡기면 '만들어' 가 이겨서
|
|
169
|
+
// 계획을 건너뛴다 — 그게 바로 이 길을 만든 이유다.
|
|
170
|
+
const 겹 = 겹친요청(s);
|
|
171
|
+
if (겹.겹침) return { mode: 'plan', score: 0, why: 겹.why, 겹침: true, 점수들: {} };
|
|
172
|
+
|
|
122
173
|
const 점수들 = {};
|
|
123
174
|
const 근거 = {};
|
|
124
175
|
|
|
@@ -138,19 +189,20 @@ export function route(text) {
|
|
|
138
189
|
const 순위 = Object.entries(점수들).sort((a, b) => b[1] - a[1]);
|
|
139
190
|
const [으뜸, 점수] = 순위[0] ?? [null, 0];
|
|
140
191
|
if (!으뜸 || 점수 < (문턱[으뜸] ?? 4)) {
|
|
141
|
-
return { mode: null, score: 점수, why: '무슨 일인지 뚜렷하지 않음', 점수들 };
|
|
192
|
+
return { mode: null, score: 점수, why: '무슨 일인지 뚜렷하지 않음', 겹침: false, 점수들 };
|
|
142
193
|
}
|
|
143
194
|
|
|
144
195
|
// 1·2등이 붙으면 고르지 않는다. 반반인 것을 억지로 정하면 절반은 틀린다.
|
|
145
196
|
const 둘째 = 순위[1]?.[1] ?? 0;
|
|
146
197
|
if (점수 - 둘째 < 2 && 둘째 >= (문턱[순위[1][0]] ?? 4)) {
|
|
147
|
-
return { mode: null, score: 점수, why: `${으뜸} 과 ${순위[1][0]} 가 비슷함`, 점수들 };
|
|
198
|
+
return { mode: null, score: 점수, why: `${으뜸} 과 ${순위[1][0]} 가 비슷함`, 겹침: false, 점수들 };
|
|
148
199
|
}
|
|
149
200
|
|
|
150
201
|
return {
|
|
151
202
|
mode: 으뜸,
|
|
152
203
|
score: 점수,
|
|
153
204
|
why: 근거[으뜸].slice(0, 2).map((x) => `'${x}'`).join(', '),
|
|
205
|
+
겹침: false,
|
|
154
206
|
점수들,
|
|
155
207
|
};
|
|
156
208
|
}
|