deel-local-cli 1.2.0 → 1.4.1
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 +301 -1537
- package/README.md +272 -1582
- package/bin/deel.js +73 -4
- package/package.json +8 -3
- package/src/acp/jsonrpc.js +230 -0
- package/src/acp/map.js +219 -0
- package/src/acp/serve.js +556 -0
- package/src/agent/card.js +129 -0
- package/src/agent/compact.js +10 -2
- package/src/agent/effort.js +5 -0
- package/src/agent/evidence.js +186 -0
- package/src/agent/grade.js +20 -0
- package/src/agent/loop.js +134 -13
- package/src/agent/models.js +169 -0
- package/src/agent/modes.js +47 -1
- package/src/agent/pins.js +140 -0
- package/src/agent/preset.js +113 -0
- package/src/agent/project.js +10 -4
- package/src/agent/session.js +233 -13
- package/src/agent/store.js +31 -0
- package/src/backend/adapter.js +14 -0
- package/src/commands.js +616 -52
- package/src/i18n/en.js +265 -0
- package/src/i18n/index.js +126 -0
- package/src/i18n/ko.js +252 -0
- package/src/lsp/client.js +459 -0
- package/src/lsp/diag.js +112 -0
- package/src/lsp/rpc.js +84 -0
- package/src/lsp/servers.js +218 -0
- package/src/oneshot.js +19 -0
- package/src/pack/sbom.js +218 -0
- package/src/pack/selfpack.js +19 -3
- package/src/preview/serve.js +19 -2
- package/src/repl.js +193 -8
- package/src/safety/secrets.js +205 -0
- package/src/safety/undo.js +10 -3
- package/src/tools/desc.en.js +221 -0
- package/src/tools/docs.js +252 -0
- package/src/tools/index.js +190 -6
- package/src/tools/lsp.js +327 -0
- package/src/tools/task.js +30 -2
- package/src/ui/ansi.js +45 -0
- package/src/ui/approve.js +25 -21
- package/src/ui/banner.js +245 -0
- package/src/ui/export.js +217 -0
- package/src/ui/inputbox.js +37 -7
- package/src/ui/intro.js +206 -0
- package/src/ui/level.js +11 -5
- package/src/ui/notify.js +101 -0
- package/src/ui/status.js +181 -35
package/src/agent/session.js
CHANGED
|
@@ -7,6 +7,8 @@ import { normalize as normLevel, DEFAULT as LEVEL_DEFAULT } from '../ui/level.js
|
|
|
7
7
|
import { 매김, 급말, 값 as 급값, 지켜본것 } from './grade.js';
|
|
8
8
|
import { 지문 } from './project.js';
|
|
9
9
|
import { 프롬프트토막 as 기억토막 } from './memory.js';
|
|
10
|
+
import { 못박기 } from './pins.js';
|
|
11
|
+
import { 언어 } from '../i18n/index.js';
|
|
10
12
|
|
|
11
13
|
// 토큰 추정 — 정확한 토크나이저 없이 대략만 센다.
|
|
12
14
|
// 한글은 글자당 약 1토큰, 영문·코드는 약 4글자당 1토큰으로 본다.
|
|
@@ -74,6 +76,71 @@ const BASE_RULES_짧게 = `너는 deel 다. 사용자의 작업 폴더에서 코
|
|
|
74
76
|
- 답은 한국어로 짧게. 코드를 통째로 붙여넣지 마라.
|
|
75
77
|
- 사용자가 정한 규칙은 Remember 로 한 줄 남긴다. "저번에" 라고 하면 Recall 로 찾는다.`;
|
|
76
78
|
|
|
79
|
+
/*
|
|
80
|
+
* ── 영어판 ──────────────────────────────────────────────────────────────
|
|
81
|
+
*
|
|
82
|
+
* /lang en 일 때 **모델이 읽는 글도** 영어로 간다. 화면 말만 바꾸는 1단계와
|
|
83
|
+
* 여기가 다른 점이고, 다르게 한 데는 두 가지 이유가 있다.
|
|
84
|
+
*
|
|
85
|
+
* 1) 안 바꾸면 답이 한국어로 온다. 위 규칙에 "사용자에게 답할 때는 한국어로"
|
|
86
|
+
* 가 박혀 있어서다. 화면 글자만 영어로 갈아 끼워 놓고 모델은 계속 한국어로
|
|
87
|
+
* 답하면, 영어권 사람에게는 아무것도 안 고친 것과 같다.
|
|
88
|
+
*
|
|
89
|
+
* 2) 토큰이 눈에 띄게 싸다. 한글은 글자당 약 1토큰이고 영문·코드는 약 3.6자당
|
|
90
|
+
* 1토큰이다(estimateTokens 를 볼 것). 32k 창에서 고정 몫이 15% 를 먹고
|
|
91
|
+
* 있었는데, 그 몫이 줄면 그만큼 대화가 쓸 자리가 는다. 작은 창에서는
|
|
92
|
+
* 이게 '파일 한 개를 더 읽을 수 있나' 를 가르는 크기다.
|
|
93
|
+
*
|
|
94
|
+
* 규칙 자체는 한 줄도 안 뺐다. 옮기면서 규칙이 느슨해지면 영어로 켠 사람만
|
|
95
|
+
* 다른 프로그램을 쓰는 셈이 된다 — 특히 "확인 못 했으면 확인 못 했다고 말해라"
|
|
96
|
+
* 같은 줄은 이 프로그램이 거짓말을 안 하게 하는 자리라 글자 그대로 옮겼다.
|
|
97
|
+
*/
|
|
98
|
+
const BASE_RULES_EN = `You are deel, a tool that reads and edits code inside the user's working folder.
|
|
99
|
+
|
|
100
|
+
**Finish the job.** Do not stop at a plan.
|
|
101
|
+
|
|
102
|
+
- Start now. Create missing files and folders — that is part of the job.
|
|
103
|
+
- Ask back **only when no tool can tell you**. If you can decide, decide, and say what you decided it from.
|
|
104
|
+
- If the job needs several files, make them all. Do not touch one and stop.
|
|
105
|
+
- When you are done, check. Run it, and fix it if it fails. If you could not check, say "I could not verify this."
|
|
106
|
+
|
|
107
|
+
Rules:
|
|
108
|
+
- Do not guess — confirm with a tool. Always Read an existing file before editing it.
|
|
109
|
+
- Edit's old_string must match the file exactly, whitespace and indentation included. Do not trim it short; include plenty of surrounding context.
|
|
110
|
+
- Do not put a large file in one call. Write the first ~300 lines, then call Append repeatedly until the rest is in place.
|
|
111
|
+
Append needs no Read first. Do not resend the earlier part when appending — it will just get cut at the same place again.
|
|
112
|
+
- Do not call the same tool with the same arguments twice. The result will be the same. Remember what you saw and move on.
|
|
113
|
+
- If the user names the scope to look at, stay inside it. Otherwise look as far as you need.
|
|
114
|
+
- Use Bash when you need to run a command. Commands that cannot be undone are blocked, so find another way.
|
|
115
|
+
- Answer the user in English, briefly. Do not paste whole files back — say what changed.
|
|
116
|
+
|
|
117
|
+
Keep what will be needed again:
|
|
118
|
+
- When the user sets a rule or tells you not to do something, leave one line with Remember. That line rides on every
|
|
119
|
+
later request, so keep it to one sentence. Do not record anything that only applies to this one job.
|
|
120
|
+
- When the user points back ("last time", "as we decided"), search with Recall before asking again.
|
|
121
|
+
- When you finish a procedure you will do again, write it to .deel/skills/<name>/SKILL.md.
|
|
122
|
+
Put name and description in the front matter (fenced with ---) and the steps below. If you find a mistake in a
|
|
123
|
+
skill you used, fix that file.`;
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* 작은 창을 위한 짧은 영어판. 위 짧은 판과 같은 생각이다 —
|
|
127
|
+
* 빠진 규칙은 없고 설득하는 문장만 없다.
|
|
128
|
+
*/
|
|
129
|
+
const BASE_RULES_짧게_EN = `You are deel. You read and edit code in the user's working folder.
|
|
130
|
+
|
|
131
|
+
Finish the job. Do not stop at a plan.
|
|
132
|
+
- Start now. Create missing files and folders.
|
|
133
|
+
- If a tool can tell you, decide instead of asking. Say what you decided it from.
|
|
134
|
+
- If there are several files, make them all. Do not do one and stop.
|
|
135
|
+
- Verify before you finish. If you could not verify, say so.
|
|
136
|
+
|
|
137
|
+
- Read a file before you edit it.
|
|
138
|
+
- Edit's old_string must match the file exactly, whitespace included. Include plenty of context.
|
|
139
|
+
- For a long file, Write the first part and Append the rest. Do not resend the earlier part.
|
|
140
|
+
- Do not call the same tool with the same arguments twice. The result will be the same.
|
|
141
|
+
- Answer in English, briefly. Do not paste whole files.
|
|
142
|
+
- Record rules the user sets with Remember. When they say "last time", search with Recall.`;
|
|
143
|
+
|
|
77
144
|
/**
|
|
78
145
|
* 이 창 크기에 맞는 기본 규칙.
|
|
79
146
|
*
|
|
@@ -82,7 +149,9 @@ const BASE_RULES_짧게 = `너는 deel 다. 사용자의 작업 폴더에서 코
|
|
|
82
149
|
* 같이 움직여야 '작은 창에서는 고정 몫을 줄인다' 가 한 가지 결정이 된다.
|
|
83
150
|
*/
|
|
84
151
|
function 기본규칙(ctx) {
|
|
85
|
-
|
|
152
|
+
const 짧게 = Number(ctx) > 0 && Number(ctx) < 24000;
|
|
153
|
+
if (언어() === 'en') return 짧게 ? BASE_RULES_짧게_EN : BASE_RULES_EN;
|
|
154
|
+
return 짧게 ? BASE_RULES_짧게 : BASE_RULES;
|
|
86
155
|
}
|
|
87
156
|
|
|
88
157
|
export class Session {
|
|
@@ -118,9 +187,35 @@ export class Session {
|
|
|
118
187
|
this.보정잰것 = 0;
|
|
119
188
|
// 겪어 본 것 요약 (agent/evolve.js). 켤 때 repl 이 채운다.
|
|
120
189
|
this.배움요약 = null;
|
|
190
|
+
/*
|
|
191
|
+
* 못 박은 것 (agent/pins.js).
|
|
192
|
+
*
|
|
193
|
+
* 여기에 두는 것이 핵심이다. messages 안에 넣으면 접기와 요약이 언젠가
|
|
194
|
+
* 가져간다 — 그래서 아예 그 바깥, 시스템 프롬프트 쪽에 둔다.
|
|
195
|
+
*/
|
|
196
|
+
this.못박은것 = new 못박기();
|
|
121
197
|
this.filesRead = new Map(); // 경로 → 추정 토큰
|
|
122
198
|
this.changes = new Map(); // 경로 → {added, removed, times}. /diff 가 본다
|
|
199
|
+
/*
|
|
200
|
+
* 상태줄이 보는 두 숫자.
|
|
201
|
+
*
|
|
202
|
+
* 여기 들고 있는 이유는 **화면을 그릴 때마다 디스크를 읽지 않기 위해서**다.
|
|
203
|
+
* 상태줄은 사람이 글자 하나 칠 때마다 다시 그려진다. 거기서 되돌리기
|
|
204
|
+
* 이력 파일을 열면 타이핑이 끊긴다 — 화면 꾸미기가 입력을 느리게 만드는
|
|
205
|
+
* 것만큼 나쁜 것이 없다. repl 이 턴이 끝날 때 한 번씩 채워 준다.
|
|
206
|
+
*/
|
|
207
|
+
this.되돌릴턴 = 0;
|
|
208
|
+
this.검증 = { 돈횟수: 0, 확인: 0, 탈: 0 };
|
|
123
209
|
this.skills = []; // 켜질 때 이 PC 에서 찾은 것들
|
|
210
|
+
/*
|
|
211
|
+
* 이 자리에 언어 서버가 있나 (Def·Refs 를 목록에 넣을지).
|
|
212
|
+
*
|
|
213
|
+
* 켤 때 repl 이 한 번 재서 넣어 준다. 여기서 직접 안 재는 이유는 폴더를
|
|
214
|
+
* 훑어야 알 수 있어서다 — 세션은 시험에서도 수없이 만들어지는데, 그때마다
|
|
215
|
+
* 폴더를 훑으면 시험이 느려지고 그 자리에 뭐가 깔렸는지에 따라 결과가
|
|
216
|
+
* 달라진다. 기본은 꺼짐이고, 켜 주는 자리가 딱 하나다.
|
|
217
|
+
*/
|
|
218
|
+
this.lsp = false;
|
|
124
219
|
this.commands = [];
|
|
125
220
|
this.plugins = [];
|
|
126
221
|
this.maxSkillsListed = 40; // 프롬프트에 올릴 최대 개수
|
|
@@ -186,13 +281,13 @@ export class Session {
|
|
|
186
281
|
급값() { return 급값(this.급().급); }
|
|
187
282
|
|
|
188
283
|
systemPrompt() {
|
|
284
|
+
const 영 = 언어() === 'en';
|
|
189
285
|
const parts = [기본규칙(this.conn?.ctx)];
|
|
190
|
-
|
|
286
|
+
// 범위를 못 박는 줄. 이건 모델이 읽는 글이라 화면 말을 따라간다.
|
|
287
|
+
parts.push(영
|
|
288
|
+
? `\nWorking folder: ${this.root}\nYou can neither read nor write files outside this folder.`
|
|
289
|
+
: `\n작업 폴더: ${this.root}\n이 폴더 밖의 파일은 읽지도 쓰지도 못한다.`);
|
|
191
290
|
|
|
192
|
-
// 지금 무슨 일을 하는 중인지. 도구 목록도 이 모드에 맞춰 이미 걸러져 있다.
|
|
193
|
-
const w = workMode(this.effectiveWork());
|
|
194
|
-
// 창이 좁으면 짧은 판을 쓴다 (modes.js 의 말()). 규칙은 같고 설득하는 문장만 빠진다.
|
|
195
|
-
parts.push(`\n--- 지금 모드: ${w.name} (${w.en}) ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`);
|
|
196
291
|
/*
|
|
197
292
|
* 모델 급에 맞춘 한 문단 (grade.js).
|
|
198
293
|
*
|
|
@@ -211,7 +306,13 @@ export class Session {
|
|
|
211
306
|
*/
|
|
212
307
|
if (this.프로젝트) parts.push(this.프로젝트);
|
|
213
308
|
|
|
214
|
-
if (this.rules)
|
|
309
|
+
if (this.rules) {
|
|
310
|
+
// 사용자 규칙 파일의 **내용은 안 건드린다.** 사람이 쓴 글이고, 그 사람의
|
|
311
|
+
// 말로 모델에게 가야 한다. 여기서 바뀌는 것은 그것을 소개하는 머리말뿐이다.
|
|
312
|
+
parts.push(영
|
|
313
|
+
? `\n--- ${this.rules.name} (user rules — these win over the principles above) ---\n${this.rules.text}`
|
|
314
|
+
: `\n--- ${this.rules.name} (사용자 규칙, 위 원칙보다 우선) ---\n${this.rules.text}`);
|
|
315
|
+
}
|
|
215
316
|
|
|
216
317
|
/*
|
|
217
318
|
* 지난 대화에서 정한 것.
|
|
@@ -237,16 +338,52 @@ export class Session {
|
|
|
237
338
|
const listed = this.listedSkills();
|
|
238
339
|
if (listed.length) {
|
|
239
340
|
parts.push(
|
|
240
|
-
|
|
241
|
-
|
|
341
|
+
(영
|
|
342
|
+
? '\n--- skills available ---\nCall the Skill tool with a name to get its body. If none fits, just carry on.\n'
|
|
343
|
+
: '\n--- 쓸 수 있는 스킬 ---\n필요한 것이 있으면 Skill 도구로 이름을 불러 본문을 받아라. 없으면 그냥 진행해라.\n') +
|
|
242
344
|
// 설명이 없는 스킬이 섞일 수 있다 — 남의 폴더에서 오는 파일이라
|
|
243
345
|
// 앞머리(frontmatter)가 빠지곤 한다. 여기서 터지면 시스템 프롬프트를
|
|
244
346
|
// 못 만들어 **매 턴** 죽는다. 목록 명령 하나가 아니라 대화 전체가 막힌다.
|
|
245
347
|
listed.map((s) => `- ${s.name}: ${String(s.description ?? '').slice(0, this.maxSkillDesc)}`).join('\n')
|
|
246
348
|
);
|
|
247
349
|
const rest = this.skills.filter((s) => s.enabled).length - listed.length;
|
|
248
|
-
if (rest > 0)
|
|
350
|
+
if (rest > 0) {
|
|
351
|
+
parts.push(영
|
|
352
|
+
? `(${rest} more exist but did not fit.)`
|
|
353
|
+
: `(그 밖에 ${rest}개가 더 있으나 자리가 모자라 안 실었다.)`);
|
|
354
|
+
}
|
|
249
355
|
}
|
|
356
|
+
/*
|
|
357
|
+
* 지금 무슨 일을 하는 중인지 — **일부러 끝쪽에** 둔다.
|
|
358
|
+
*
|
|
359
|
+
* 이 절은 이 프롬프트에서 유일하게 **매 턴 바뀔 수 있는** 자리다. 말을
|
|
360
|
+
* 던질 때마다 알맞은 모드로 저절로 옮겨 가기 때문이다(route.js). 그런데
|
|
361
|
+
* Ollama·llama.cpp 의 프리픽스 캐시는 앞부분이 지난 요청과 같을 때만
|
|
362
|
+
* 계산을 재쓴다 — 이 절이 앞쪽에 있으면 모드가 바뀌는 순간 그 뒤 전부,
|
|
363
|
+
* 시스템 프롬프트 나머지에 대화 전체까지 다시 계산된다. 긴 대화일수록
|
|
364
|
+
* 매 턴 몇천 토큰이고, 로컬에서는 그게 그대로 몇 초다.
|
|
365
|
+
*
|
|
366
|
+
* 그래서 변하지 않는 것들(규칙·폴더·급말·지문·사용자 규칙·기억·스킬)을
|
|
367
|
+
* 앞에 굳히고 이 절을 뒤로 보냈다. 읽기 쪽으로도 손해가 아니다 — 끝자리는
|
|
368
|
+
* 가운데보다 오히려 잘 읽힌다(lost in the middle 의 반대편이다).
|
|
369
|
+
* 이 차례는 test/cache.test.js 가 지킨다.
|
|
370
|
+
*
|
|
371
|
+
* 도구 목록도 이 모드에 맞춰 이미 걸러져 있다. 창이 좁으면 짧은 판을
|
|
372
|
+
* 쓴다 (modes.js 의 말()) — 규칙은 같고 설득하는 문장만 빠진다.
|
|
373
|
+
*/
|
|
374
|
+
const w = workMode(this.effectiveWork());
|
|
375
|
+
parts.push(영
|
|
376
|
+
? `\n--- current mode: ${w.en} ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`
|
|
377
|
+
: `\n--- 지금 모드: ${w.name} (${w.en}) ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`);
|
|
378
|
+
/*
|
|
379
|
+
* 못 박은 것은 **맨 끝**에 붙인다 (agent/pins.js).
|
|
380
|
+
*
|
|
381
|
+
* 긴 글의 가운데는 흘려 읽힌다 — 'lost in the middle' 이라 부르는 것이고,
|
|
382
|
+
* 어느 모델에서나 잰다. 사람이 직접 못 박은 말은 그 가운데에 묻히면 안 되므로
|
|
383
|
+
* 가장 마지막, 대화 바로 앞에 둔다. 모드 절보다도 뒤인 것도 그래서다.
|
|
384
|
+
*/
|
|
385
|
+
const 못박은글 = this.못박은것?.요약();
|
|
386
|
+
if (못박은글) parts.push(못박은글);
|
|
250
387
|
return parts.join('\n');
|
|
251
388
|
}
|
|
252
389
|
|
|
@@ -260,8 +397,90 @@ export class Session {
|
|
|
260
397
|
.slice(0, this.maxSkillsListed);
|
|
261
398
|
}
|
|
262
399
|
|
|
263
|
-
|
|
264
|
-
|
|
400
|
+
/*
|
|
401
|
+
* ── 턴이 어디서 시작했는지 ────────────────────────────────────────────
|
|
402
|
+
*
|
|
403
|
+
* 되돌리기(/undo)는 파일만 되돌렸다. 대화에는 "src/runner.js 를 고쳤습니다" 가
|
|
404
|
+
* 그대로 남아 있어서, 되돌린 다음 턴에 모델은 **이미 고쳐 놓은 줄 알고** 그
|
|
405
|
+
* 위에 이어 일했다 — 없는 코드를 고치려 들고, 없는 함수를 부른다. 사람 눈에는
|
|
406
|
+
* 모델이 헛소리하는 것으로 보이지만, 사실은 우리가 모델에게 거짓말을 남겨 둔
|
|
407
|
+
* 것이다. 그러니 파일을 되감을 때 말도 같이 걷어내야 한다.
|
|
408
|
+
*
|
|
409
|
+
* 자리를 **숫자로 적어 두지 않는다.** 접기(compact)와 줄이기(trim)가 messages
|
|
410
|
+
* 를 통째로 갈아 끼우기 때문에, 적어 둔 3번은 다음 순간 엉뚱한 말을 가리킨다.
|
|
411
|
+
* 잘못된 자리에서 자르는 되돌리기는 안 하느니만 못하다. 그래서 **메시지 객체
|
|
412
|
+
* 자체**를 들고 있다가 그때그때 indexOf 로 찾는다. 접혀서 사라졌으면 못 찾고,
|
|
413
|
+
* 못 찾으면 그 턴은 되감을 수 없다고 정직하게 말한다.
|
|
414
|
+
*/
|
|
415
|
+
#턴표 = [];
|
|
416
|
+
#다음턴 = null;
|
|
417
|
+
static #표최대 = 200;
|
|
418
|
+
|
|
419
|
+
/** 새 턴을 연다. 바로 다음에 push 되는 말이 이 턴의 첫 말이 된다. */
|
|
420
|
+
턴시작(턴) {
|
|
421
|
+
if (턴 == null) return this;
|
|
422
|
+
// 접혀 없어진 표는 여기서 턴다 — 안 그러면 긴 대화에서 끝없이 쌓인다.
|
|
423
|
+
this.#턴표 = this.#턴표.filter((x) => this.messages.includes(x.표));
|
|
424
|
+
if (this.#턴표.length > Session.#표최대) this.#턴표 = this.#턴표.slice(-Session.#표최대);
|
|
425
|
+
this.#다음턴 = 턴;
|
|
426
|
+
return this;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** 살아 있는 턴 표시들. 접혀 사라진 것은 빠진다. @returns {{턴:number, 자리:number}[]} */
|
|
430
|
+
턴자리() {
|
|
431
|
+
const out = [];
|
|
432
|
+
for (const x of this.#턴표) {
|
|
433
|
+
const i = this.messages.indexOf(x.표);
|
|
434
|
+
if (i >= 0) out.push({ 턴: x.턴, 자리: i });
|
|
435
|
+
}
|
|
436
|
+
return out.sort((a, b) => a.자리 - b.자리);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 주어진 턴들의 말을 걷어낸다.
|
|
441
|
+
*
|
|
442
|
+
* 여러 턴이면 그중 **제일 이른** 자리까지 간다 — 그 뒤는 어차피 되돌린 파일
|
|
443
|
+
* 위에서 나눈 이야기라 남겨 둘 이유가 없다. 사람이 쳤던 말은 돌려준다,
|
|
444
|
+
* 다시 치기 쉽게.
|
|
445
|
+
*
|
|
446
|
+
* 자르고 나서 repairToolPairs 를 반드시 한 번 돌린다. 도구를 부른 assistant
|
|
447
|
+
* 만 남고 그 결과가 없으면 그 뒤 모든 요청이 400 으로 튕긴다 — 되돌리기가
|
|
448
|
+
* 대화를 아예 못 쓰게 만드는 셈이다. 턴 경계는 보통 깨끗하지만, 여기서만은
|
|
449
|
+
* '보통' 에 기대지 않는다.
|
|
450
|
+
*/
|
|
451
|
+
되감기(턴들) {
|
|
452
|
+
const 찾을것 = new Set((Array.isArray(턴들) ? 턴들 : []).filter((t) => t != null));
|
|
453
|
+
const 빈것 = { 걷은것: 0, 고친것: 0, 사람말: null, 턴: [] };
|
|
454
|
+
if (!찾을것.size) return 빈것;
|
|
455
|
+
|
|
456
|
+
const 표들 = this.턴자리().filter((x) => 찾을것.has(x.턴));
|
|
457
|
+
if (!표들.length) return 빈것;
|
|
458
|
+
|
|
459
|
+
const 자리 = 표들[0].자리;
|
|
460
|
+
const 첫말 = this.messages[자리];
|
|
461
|
+
const 사람말 = 첫말?.role === 'user' && typeof 첫말.content === 'string' ? 첫말.content : null;
|
|
462
|
+
|
|
463
|
+
const 전 = this.messages.length;
|
|
464
|
+
const 고침 = repairToolPairs(this.messages.slice(0, 자리));
|
|
465
|
+
this.messages = 고침.messages;
|
|
466
|
+
this.#턴표 = this.#턴표.filter((x) => this.messages.includes(x.표));
|
|
467
|
+
this.#다음턴 = null;
|
|
468
|
+
return { 걷은것: 전 - this.messages.length, 고친것: 고침.고친것, 사람말, 턴: 표들.map((x) => x.턴) };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
push(msg) {
|
|
472
|
+
if (this.#다음턴 != null) { this.#턴표.push({ 턴: this.#다음턴, 표: msg }); this.#다음턴 = null; }
|
|
473
|
+
this.messages.push(msg);
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
clear() {
|
|
478
|
+
this.messages = [];
|
|
479
|
+
this.filesRead.clear();
|
|
480
|
+
this.#턴표 = [];
|
|
481
|
+
this.#다음턴 = null;
|
|
482
|
+
return this;
|
|
483
|
+
}
|
|
265
484
|
|
|
266
485
|
noteRead(path, text) { this.filesRead.set(path, estimateTokens(text)); }
|
|
267
486
|
|
|
@@ -408,7 +627,7 @@ export class Session {
|
|
|
408
627
|
const mcp수 = (this.mcp ?? []).reduce((n, s) => n + (s.도구?.length ?? 0), 0);
|
|
409
628
|
// 창 크기도 열쇠에 넣는다. 설명을 창에 맞춰 줄여 싣기 때문에(budget.js),
|
|
410
629
|
// /ctx 로 창을 다시 잡으면 이 값도 달라져야 한다. 안 넣으면 옛 값이 남는다.
|
|
411
|
-
const 열쇠 = `${this.effectiveWork()}|${this.skills?.length ? 'skill' : ''}|${this.web !== false ? 'web' : ''}|mcp${mcp수}|c${this.conn?.ctx ?? 0}`;
|
|
630
|
+
const 열쇠 = `${this.effectiveWork()}|${this.skills?.length ? 'skill' : ''}|${this.web !== false ? 'web' : ''}|${this.lsp ? 'lsp' : ''}|mcp${mcp수}|c${this.conn?.ctx ?? 0}`;
|
|
412
631
|
if (this.#도구잰것.has(열쇠)) return this.#도구잰것.get(열쇠);
|
|
413
632
|
let n = 0;
|
|
414
633
|
try {
|
|
@@ -417,6 +636,7 @@ export class Session {
|
|
|
417
636
|
web: this.web !== false,
|
|
418
637
|
work: this.effectiveWork(),
|
|
419
638
|
mcp: this.mcp ?? null,
|
|
639
|
+
lsp: this.lsp === true,
|
|
420
640
|
// 실제로 나가는 것과 **같은 것**을 재야 한다. 안 넘기면 안 줄인 것을
|
|
421
641
|
// 재게 되고, 그러면 /context 가 실제보다 크게 말한다 — 그 값으로
|
|
422
642
|
// effort.js 가 출력 상한을 잡으므로 답이 이유 없이 짧아진다.
|
package/src/agent/store.js
CHANGED
|
@@ -98,12 +98,43 @@ export class Store {
|
|
|
98
98
|
this.#write({ t: 'msg', m: msg });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* 못 박은 것을 적어 둔다. 통째로 한 줄 — 마지막 것이 이긴다.
|
|
103
|
+
*
|
|
104
|
+
* 메시지와 따로 두는 이유는 pins.js 에 적은 것과 같다. 접기·요약은 메시지를
|
|
105
|
+
* 손보는데, 못 박은 것은 그 손질에 닿으면 안 된다.
|
|
106
|
+
*/
|
|
107
|
+
못박기목록(목록) {
|
|
108
|
+
this.#open();
|
|
109
|
+
this.#write({ t: 'pins', at: new Date().toISOString(), 목록: 목록 ?? [] });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 마지막으로 적힌 못 박은 것. 없으면 빈 배열. */
|
|
113
|
+
못박은것읽기() {
|
|
114
|
+
if (!existsSync(this.file)) return [];
|
|
115
|
+
let 마지막 = [];
|
|
116
|
+
try {
|
|
117
|
+
for (const line of readFileSync(this.file, 'utf8').split('\n')) {
|
|
118
|
+
if (!line.includes('"t":"pins"')) continue;
|
|
119
|
+
try {
|
|
120
|
+
const j = JSON.parse(line);
|
|
121
|
+
if (j.t === 'pins' && Array.isArray(j.목록)) 마지막 = j.목록;
|
|
122
|
+
} catch { /* 깨진 줄은 건너뛴다 */ }
|
|
123
|
+
}
|
|
124
|
+
} catch { return []; }
|
|
125
|
+
return 마지막;
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
// 압축이 일어나면 이력이 통째로 바뀐다. 그때는 새로 적는다.
|
|
102
129
|
replace(messages, note = '압축') {
|
|
103
130
|
this.#open();
|
|
104
131
|
const meta = this.readMeta() ?? {};
|
|
132
|
+
// 여기가 놓치기 쉬운 자리다. 파일을 새로 쓰면서 못 박은 것을 안 옮기면,
|
|
133
|
+
// '요약해도 안 지워진다' 는 말이 바로 그 요약에서 거짓이 된다.
|
|
134
|
+
const 못박은것 = this.못박은것읽기();
|
|
105
135
|
const lines = [JSON.stringify({ t: 'meta', at: new Date().toISOString(), ...meta })];
|
|
106
136
|
lines.push(JSON.stringify({ t: 'note', at: new Date().toISOString(), note }));
|
|
137
|
+
if (못박은것.length) lines.push(JSON.stringify({ t: 'pins', at: new Date().toISOString(), 목록: 못박은것 }));
|
|
107
138
|
for (const m of messages) lines.push(JSON.stringify({ t: 'msg', m }));
|
|
108
139
|
try { writeFileSync(this.file, lines.join('\n') + '\n', 'utf8'); } catch {}
|
|
109
140
|
}
|
package/src/backend/adapter.js
CHANGED
|
@@ -20,6 +20,20 @@ export function buildBody(shape, { model, messages, tools, stream, json, think,
|
|
|
20
20
|
* 지금까지 이 값을 **한 번도 안 보냈다.** 보내면 그 길이로 올려 준다.
|
|
21
21
|
*/
|
|
22
22
|
if (ctx) body.options.num_ctx = ctx;
|
|
23
|
+
/*
|
|
24
|
+
* 모델을 내리지 말라고 말해 둔다.
|
|
25
|
+
*
|
|
26
|
+
* Ollama 는 5분 동안 조용하면 모델을 내린다. 다음 말을 걸면 다시 올리고
|
|
27
|
+
* **대화 전체를 다시 계산한다** — 프리픽스 캐시가 통째로 사라진 것과 같다.
|
|
28
|
+
* 로컬 대화는 사람이 생각하고 다른 창을 보다 돌아오는 것이라, 5분 넘게
|
|
29
|
+
* 조용한 것이 오히려 보통이다. 그 복귀 첫 마디가 제일 오래 걸리는 이유가
|
|
30
|
+
* 바로 이것이었다.
|
|
31
|
+
*
|
|
32
|
+
* -1(영원히)은 안 쓴다. 모델을 갈아탄 뒤에도 이전 모델이 램을 물고 있게
|
|
33
|
+
* 되는데, 8GB 램에서는 다음 모델이 못 올라온다는 뜻이다. 한 시간이면
|
|
34
|
+
* 일하는 호흡은 다 덮고, 퇴근하면 놓아 준다. DEEL_KEEP_ALIVE 로 바꾼다.
|
|
35
|
+
*/
|
|
36
|
+
body.keep_alive = process.env.DEEL_KEEP_ALIVE || '60m';
|
|
23
37
|
if (tools?.length) body.tools = tools;
|
|
24
38
|
if (json) body.format = json;
|
|
25
39
|
if (think !== undefined) body.think = think;
|