deel-local-cli 1.8.0 → 1.12.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.
Files changed (61) hide show
  1. package/README.ko.md +10 -25
  2. package/README.md +43 -51
  3. package/bin/deel.js +56 -13
  4. package/package.json +60 -60
  5. package/src/acp/serve.js +810 -732
  6. package/src/agent/budget.js +156 -153
  7. package/src/agent/compact.js +420 -296
  8. package/src/agent/effort.js +308 -177
  9. package/src/agent/evidence.js +2 -0
  10. package/src/agent/evolve.js +263 -213
  11. package/src/agent/filemem.js +155 -0
  12. package/src/agent/grade.js +51 -1
  13. package/src/agent/loop.js +1707 -1349
  14. package/src/agent/memory.js +156 -152
  15. package/src/agent/mention.js +210 -210
  16. package/src/agent/session.js +1049 -832
  17. package/src/agent/store.js +186 -9
  18. package/src/agent/threads.js +26 -1
  19. package/src/backend/adapter.js +1159 -615
  20. package/src/backend/cachemark.js +150 -0
  21. package/src/backend/detect.js +327 -288
  22. package/src/backend/learn.js +24 -0
  23. package/src/backend/mcp.js +96 -5
  24. package/src/backend/probe.js +131 -61
  25. package/src/backend/quota.js +250 -133
  26. package/src/backend/retry.js +30 -6
  27. package/src/backend/tokens.js +37 -0
  28. package/src/backend/toolfit.js +352 -0
  29. package/src/backend/wire.js +600 -0
  30. package/src/commands.js +3135 -2918
  31. package/src/config.js +47 -2
  32. package/src/i18n/en.js +544 -466
  33. package/src/i18n/index.js +18 -0
  34. package/src/i18n/ja.js +497 -417
  35. package/src/i18n/ko.js +591 -466
  36. package/src/i18n/zh.js +497 -417
  37. package/src/lsp/client.js +49 -5
  38. package/src/oneshot.js +620 -471
  39. package/src/pack/sbom.js +30 -4
  40. package/src/pack/selfpack.js +25 -9
  41. package/src/pack/sheet.en.js +288 -0
  42. package/src/pack/tar.js +65 -2
  43. package/src/plugins/manage.js +46 -9
  44. package/src/providers/bedrock.js +17 -0
  45. package/src/repl.js +2602 -2286
  46. package/src/safety/audit.js +92 -8
  47. package/src/safety/authcmd.js +14 -3
  48. package/src/safety/guard.js +143 -0
  49. package/src/safety/keystore.js +62 -39
  50. package/src/safety/undo.js +24 -6
  51. package/src/tools/fsutil.js +265 -250
  52. package/src/tools/index.js +239 -34
  53. package/src/tools/jobs.js +158 -29
  54. package/src/tools/verify.js +358 -328
  55. package/src/tools/webfetch.js +92 -9
  56. package/src/ui/md.js +201 -5
  57. package/src/ui/motion.js +0 -1
  58. package/src/ui/pastechip.js +50 -3
  59. package/src/ui/pick.js +115 -0
  60. package/src/ui/screen.js +23 -3
  61. package/src/ui/status.js +613 -610
@@ -0,0 +1,600 @@
1
+ // 전선 카드 — 이 모델이 이 주소에서 **실제로 받는 것**.
2
+ //
3
+ // ── 왜 필요한가 ─────────────────────────────────────────────────────────
4
+ //
5
+ // 여태 규격은 셋이었다(openai · anthropic · ollama). 그런데 같은 규격 안에서도
6
+ // 받는 것이 다르다. 실제로 겪은 세 자리다.
7
+ //
8
+ // 1. `/think max` 를 쳐도 전선에는 `high` 가 나갔다. openai 규격 눈금표가
9
+ // max 를 high 로 뭉개고 있었다. 화면은 max, 전선은 high — 사람은 세게
10
+ // 생각하라고 시켰다고 믿는데 아무 일도 안 일어난다.
11
+ //
12
+ // 2. Anthropic 규격에 생각을 켜면 `budget_tokens` 를 보냈다. Opus 5 ·
13
+ // Opus 4.7 · 4.8 · Fable 5 는 그 칸을 **400 으로 거절한다.** 열쇠가
14
+ // 멀쩡한데 400 이니, 화면에서는 인증 실패와 구별이 안 된다.
15
+ //
16
+ // 3. 캐시 표식을 아무 데도 안 붙였다. 자동 캐시를 하는 서버는 정적 앞머리만
17
+ // 잡아 주고, 안 하는 서버(Anthropic 직통)는 **하나도 안 잡는다.**
18
+ //
19
+ // ── 표를 박지 않는다 ────────────────────────────────────────────────────
20
+ //
21
+ // 모델 이름과 그 능력을 표로 박으면 그 표는 반드시 낡는다. 이 프로그램에는
22
+ // 이미 그 문제를 푸는 방식이 있다 — **짐작하고, 서버가 거절하면 배우고,
23
+ // 배운 것을 남긴다** (backend/learn.js · agent/card.js · agent/evolve.js).
24
+ // 여기도 같은 세 단계다.
25
+ //
26
+ // 짐작 주소의 회사와 모델 이름의 세대로 첫 값을 고른다
27
+ // 배움 400 문구에서 무엇이 안 되는지 읽는다 (아래 배울전선)
28
+ // 남김 모델별로 집 파일에 적어 둔다 (agent/evolve.js)
29
+ //
30
+ // ── 세션 안에서는 안 바뀐다 ─────────────────────────────────────────────
31
+ //
32
+ // 카드는 **세션을 열 때 한 번** 정하고 끝까지 그대로 쓴다. 요청마다 값이
33
+ // 달라지면 그것이 곧 프리픽스가 흔들린다는 뜻이고, 프리픽스가 흔들리면
34
+ // 캐시가 매번 새로 엮인다. 배워서 값이 바뀌는 것은 예외다 — 그건 안 그러면
35
+ // 그 턴이 죽는 자리라, 한 번 흔들리는 값을 치른다.
36
+ import { 벤더 } from './toolfit.js';
37
+ import { 말 } from '../i18n/index.js';
38
+
39
+ /** 우리 눈금. agent/effort.js 의 LEVELS 와 같은 차례다 (off 는 따로 다룬다). */
40
+ export const 눈금차례 = ['low', 'medium', 'high', 'xhigh', 'max'];
41
+
42
+ /*
43
+ * 회사별로 전선이 받는 눈금.
44
+ *
45
+ * 우리 눈금 다섯이 어디서나 통하지는 않는다. 없는 말을 실어 보내면 400 이고,
46
+ * 그 400 은 화면에서 열쇠가 틀린 것과 구별이 안 된다. 그래서 회사마다 받는
47
+ * 말만 적어 두고, 없는 말은 **있는 말 중 가장 가까운 아래쪽**으로 내린다.
48
+ *
49
+ * Claude low · medium · high · xhigh · max (xhigh 가 코딩에 제일 낫다)
50
+ * OpenAI minimal · low · medium · high
51
+ * Gemini none · low · medium · high
52
+ * Ollama 참·거짓, 또는 low · medium · high
53
+ *
54
+ * 회사를 못 알아본 주소에는 **아무것도 더 안 보낸다.** 짐작으로 실은 칸
55
+ * 하나가 그 턴을 죽인다.
56
+ *
57
+ * 다만 이 표가 비었다고 눈금이 늘 비는 것은 아니다. 규격을 알아본 자리에는
58
+ * 그 규격이 확실히 받는 눈금을 채운다(아래 기본카드의 anthropic·ollama 갈래).
59
+ * 회사를 모르는 것과 규격을 모르는 것은 다른 이야기라서다 — 규격이 anthropic
60
+ * 이면 어느 게이트웨이를 거치든 low·medium·high 는 받는다.
61
+ */
62
+ const 회사눈금 = {
63
+ anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
64
+ bedrock: ['low', 'medium', 'high', 'xhigh', 'max'],
65
+ openai: ['minimal', 'low', 'medium', 'high'],
66
+ azure: ['low', 'medium', 'high'],
67
+ gemini: ['none', 'low', 'medium', 'high'],
68
+ };
69
+
70
+ /** '생각 끄기' 를 이 회사는 뭐라고 하나. 없으면 칸 자체를 안 보낸다. */
71
+ const 끄는말 = { openai: 'minimal', gemini: 'none' };
72
+
73
+ /**
74
+ * 모델 이름에서 세대를 읽는다.
75
+ *
76
+ * 이름 짓는 법이 두 가지라 둘 다 본다.
77
+ * 새 방식 claude-opus-5 · claude-opus-4-8 · anthropic.claude-sonnet-4-6-v1
78
+ * 옛 방식 claude-3-5-sonnet-20241022
79
+ *
80
+ * @returns {number|null} 4.6 · 5 처럼. 못 읽으면 null
81
+ */
82
+ export function 세대(모델) {
83
+ const s = String(모델 ?? '').toLowerCase();
84
+ if (!s) return null;
85
+
86
+ /*
87
+ * ── 옛 방식을 먼저 본다 ─────────────────────────────────────────────
88
+ *
89
+ * 옛 이름은 숫자가 갈래 **앞**에 온다 (`claude-3-5-sonnet-20241022`).
90
+ * 새 방식을 먼저 보면 그 이름에서 `sonnet-20241022` 를 읽어 판을
91
+ * **20241022** 로 잡는다. 그 값은 4.6 보다 크니 생각을 adaptive 로
92
+ * 켜고, 그 모델은 그 칸을 400 으로 거절한다 — 옛 모델을 쓰는 사람만
93
+ * 조용히 못 쓰게 되는 자리다. 좁은 규칙을 먼저 보는 이유가 이것이다.
94
+ */
95
+ let m = /claude[-_.]?(\d+)(?:[-_.](\d+))?[-_.](opus|sonnet|haiku)/.exec(s);
96
+ if (m) return 판짜기(m[1], m[2]);
97
+
98
+ // 새 방식 — 갈래 이름 **뒤에** 숫자가 온다 (`claude-opus-4-8`).
99
+ m = /(opus|sonnet|haiku|fable|mythos)[-_.]?(\d+)(?:[-_.](\d+))?/.exec(s);
100
+ if (m) return 판짜기(m[2], m[3]);
101
+
102
+ // 판 번호 없이 갈래만 적힌 이름. Fable · Mythos 는 최신 갈래라 그렇게 친다.
103
+ if (/fable|mythos/.test(s)) return 5;
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * 큰 자리와 작은 자리를 판 번호 하나로.
109
+ *
110
+ * 작은 자리는 **한 자리만** 본다. `claude-sonnet-4-6-v1` 의 뒤 `v1` 도,
111
+ * `20241022` 같은 날짜도 판 번호가 아니다. 큰 자리도 마찬가지라 두 자리를
112
+ * 넘으면 판이 아니라 날짜로 보고 안 읽는다 — 모르는 것을 지어내면 그 짐작이
113
+ * 그대로 생각 형식을 정하고, 틀리면 그 턴이 400 이다.
114
+ */
115
+ function 판짜기(큰것, 작은것) {
116
+ const 큰 = Number(큰것);
117
+ if (!Number.isFinite(큰) || 큰 > 99) return null;
118
+ const 작은 = 작은것 === undefined ? 0 : Number(작은것);
119
+ return 큰 + (Number.isFinite(작은) && 작은 < 10 ? 작은 / 10 : 0);
120
+ }
121
+
122
+ /** 이 이름이 Claude 갈래로 보이나. */
123
+ export function 클로드인가(모델) {
124
+ return /claude|fable|mythos|opus|sonnet|haiku/i.test(String(모델 ?? ''));
125
+ }
126
+
127
+ /**
128
+ * 이 이름이 추론 눈금(reasoning_effort)을 받는 OpenAI 갈래인가.
129
+ *
130
+ * gpt-5 · o1 · o3 · o4 계열이다. gpt-4o 는 아니다 — `o` 가 붙어 있지만
131
+ * 추론 모델이 아니라서, 여기 걸리면 안 되는 값을 실어 보내게 된다.
132
+ */
133
+ export function 추론형오픈AI(모델) {
134
+ const s = String(모델 ?? '').toLowerCase();
135
+ if (/gpt-?4o/.test(s)) return false;
136
+ return /(^|[^a-z])o[1-9]([^a-z]|$)|gpt-?5|gpt-?6/.test(s);
137
+ }
138
+
139
+ /**
140
+ * 아무것도 안 겪었을 때의 카드.
141
+ *
142
+ * @param {object} conn { kind, base, model }
143
+ */
144
+ export function 기본카드(conn) {
145
+ const 회사 = 벤더(conn);
146
+ const 규격 = conn?.kind ?? 'openai';
147
+ const 모델 = String(conn?.model ?? '');
148
+
149
+ const 카드 = {
150
+ 회사,
151
+ 규격,
152
+ // 생각을 어떻게 켜나
153
+ 생각형식: 'none',
154
+ // 전선이 받는 눈금
155
+ 눈금: 회사 ? (회사눈금[회사] ?? []) : [],
156
+ 끄는말: 회사 ? (끄는말[회사] ?? null) : null,
157
+ // 눈금을 어느 칸에 싣나 — adaptive 는 output_config.effort 에 싣는다
158
+ 효력칸: null,
159
+ // 캐시 표식을 우리가 붙이나
160
+ 캐시: 'none',
161
+ // 캐시 표식이 잡히는 최소 크기 (그 아래면 붙여도 안 잡힌다 — 탈은 아니다)
162
+ 캐시최소: 1024,
163
+ // 세션 이름을 어느 칸에 싣나
164
+ 세션자리: null,
165
+ // 흘려받기에서 usage 를 달라고 할까
166
+ 스트림usage: false,
167
+ };
168
+
169
+ if (규격 === 'ollama') {
170
+ 카드.생각형식 = 'boolean';
171
+ 카드.눈금 = ['low', 'medium', 'high'];
172
+ return 카드;
173
+ }
174
+
175
+ if (규격 === 'anthropic') {
176
+ /*
177
+ * 이 규격은 캐시 표식을 **정식으로** 받는다. 붙일 수 있는 유일한 자리라
178
+ * 여기서는 망설이지 않는다 — 안 붙이면 Anthropic 직통은 캐시가 0 이다.
179
+ */
180
+ 카드.캐시 = 'explicit';
181
+ 카드.캐시최소 = 회사 === 'bedrock' ? 4096 : 1024;
182
+ 카드.세션자리 = 'metadata';
183
+ /*
184
+ * 4.6 판부터 생각이 adaptive 로 바뀌었다. 그 전 판은 budget_tokens 다.
185
+ * 못 읽으면 adaptive 로 간다 — 지금 쓰이는 것이 그쪽이고, 틀려도 첫 400
186
+ * 에서 바로 배운다(아래 배울전선).
187
+ */
188
+ const v = 세대(모델);
189
+ 카드.생각형식 = v === null ? 'adaptive' : (v >= 4.6 ? 'adaptive' : 'budget');
190
+ if (카드.생각형식 === 'adaptive') 카드.효력칸 = 'output_config';
191
+ if (!카드.눈금.length) 카드.눈금 = 회사눈금.anthropic;
192
+ return 카드;
193
+ }
194
+
195
+ // ── 여기부터 openai 규격 ────────────────────────────────────────────
196
+ /*
197
+ * 흘려받을 때 usage 를 달라고 한다.
198
+ *
199
+ * 이게 없으면 흘려받는 길에서 토큰 수도 캐시 수치도 **아예 안 온다.** 그러면
200
+ * 「캐시가 걸리고 있나」 를 잴 방법이 없고, 못 재면 고쳤는지도 모른다.
201
+ * 얻는 것이 또렷하고, 안 받는 서버는 400 문구에 `stream_options` 를 적어
202
+ * 주므로 그 자리에서 배워 끈다(아래 배울전선). 모르는 게이트웨이에는
203
+ * 처음부터 안 보낸다.
204
+ */
205
+ 카드.스트림usage = !!회사;
206
+
207
+ if (회사 === 'openai') {
208
+ /*
209
+ * 여기가 이 칸들이 **문서에 있는** 유일한 자리다.
210
+ *
211
+ * `prompt_cache_key` 로 같은 대화를 묶고, `user` 로 누구의 대화인지
212
+ * 알린다. 둘 다 이 회사 문서에 적힌 칸이다.
213
+ */
214
+ 카드.캐시 = 'key';
215
+ 카드.세션자리 = 'user';
216
+ 카드.생각형식 = 추론형오픈AI(모델) ? 'effort' : 'none';
217
+ return 카드;
218
+ }
219
+ if (회사 === 'bedrock' || 회사 === 'gemini' || 회사 === 'azure') {
220
+ /*
221
+ * 이 창구들은 서버가 알아서 앞머리를 캐시한다. 우리가 표식을 지어내
222
+ * 실어 보내는 것보다, **앞머리를 안 흔드는 것**이 여기서 할 일이다
223
+ * (agent/session.js 의 차례 · loop.js 의 전선 고정).
224
+ */
225
+ 카드.캐시 = 'auto';
226
+ /*
227
+ * ── 대화 이름은 여기에 안 싣는다 ──────────────────────────────────
228
+ *
229
+ * 이 창구들이 `user` 를 받는지 문서에서 확인하지 못했다. 그리고 받는다
230
+ * 해도 **얻는 것이 없다** — 이 창구들은 서버가 알아서 앞머리를 캐시하지,
231
+ * 이름으로 대화를 묶어 주지 않는다. 문서에 없는 칸을 얻는 것 없이 실어
232
+ * 보내는 셈이라, Bedrock 처럼 모르는 칸에 엄격한 곳에서는 그 턴이 400 이다.
233
+ *
234
+ * 여기서 캐시를 지키는 방법은 이름이 아니라 **앞머리를 안 흔드는 것**이다
235
+ * (agent/session.js 의 차례 · loop.js 의 전선 고정).
236
+ */
237
+ if (회사 === 'bedrock') {
238
+ 카드.생각형식 = 클로드인가(모델) ? 'effort' : 'none';
239
+ /*
240
+ * ── Bedrock 의 OpenAI 창구에서는 Claude 눈금을 안 쓴다 ────────────
241
+ *
242
+ * 회사는 bedrock 이라 위에서 눈금이 `…xhigh · max` 로 잡혔다. 그런데
243
+ * 지금 나가는 몸은 **OpenAI 호환 몸**이고, 그 창구가 `xhigh` 를 받는지는
244
+ * 문서에서 확인하지 못했다. 확인 못 한 말을 실어 보내면 그 턴이 400 이고,
245
+ * 그 400 은 화면에서 열쇠가 틀린 것과 구별이 안 된다 — 이 파일이 없애려던
246
+ * 바로 그 고장이다.
247
+ *
248
+ * 그래서 **몸의 규격을 따른다.** `xhigh` 와 `max` 는 Anthropic Messages
249
+ * 몸으로 나갈 때만 쓴다(위 anthropic 갈래 · mantle 의 /anthropic/v1).
250
+ *
251
+ * 잘못 좁히면 손해가 「생각을 덜 한다」 로 끝나고, 잘못 넓히면 손해가
252
+ * 「그 턴이 죽는다」 다. 확인이 안 되면 죽지 않는 쪽으로 간다.
253
+ */
254
+ 카드.눈금 = 회사눈금.openai;
255
+ } else {
256
+ 카드.생각형식 = 회사 === 'gemini' ? 'effort' : (추론형오픈AI(모델) ? 'effort' : 'none');
257
+ }
258
+ return 카드;
259
+ }
260
+
261
+ /*
262
+ * 모르는 주소 — 사내 게이트웨이가 대개 여기다.
263
+ *
264
+ * 여기서 짐작하면 안 된다. 그런데 **여태 하던 것**은 이어 가야 한다:
265
+ * 예전에도 reasoning_effort 는 보내고 있었다. 그것까지 끊으면 잘 쓰던
266
+ * 사람이 조절을 잃는다. 그래서 형식만 그대로 두고, 눈금은 제일 좁은
267
+ * 것으로 잡는다 — 없는 말을 실어 보내는 쪽이 위험하다.
268
+ */
269
+ 카드.생각형식 = 'effort';
270
+ 카드.눈금 = ['low', 'medium', 'high'];
271
+ return 카드;
272
+ }
273
+
274
+ /**
275
+ * 이 강도를 이 전선의 말로 옮긴다.
276
+ *
277
+ * 없는 말이면 **있는 것 중 가장 가까운 아래**로 내린다. 위로 올리지 않는다 —
278
+ * 사람이 시킨 것보다 세게 생각하면 값이 사람 모르게 는다.
279
+ *
280
+ * @returns {string|null} 못 옮기면 null (그때는 칸을 아예 안 보낸다)
281
+ */
282
+ export function 눈금맞추기(카드, 강도) {
283
+ const s = String(강도 ?? '');
284
+ const 눈금 = 카드?.눈금 ?? [];
285
+ if (s === 'off') return 카드?.끄는말 ?? null;
286
+ if (!눈금.length) return null;
287
+ if (눈금.includes(s)) return s;
288
+ let i = 눈금차례.indexOf(s);
289
+ if (i < 0) return null;
290
+ for (; i >= 0; i--) if (눈금.includes(눈금차례[i])) return 눈금차례[i];
291
+ return null;
292
+ }
293
+
294
+ /*
295
+ * ── 서버가 거절하면서 알려 주는 것 ──────────────────────────────────────
296
+ *
297
+ * backend/learn.js 와 같은 생각이다. 거기는 **숫자**(창 크기·출력 상한)를
298
+ * 읽고, 여기는 **칸**을 읽는다 — 무엇을 보내면 안 되는지.
299
+ *
300
+ * 제일 값진 자리는 「받는 값의 목록」 을 통째로 알려 주는 문장이다.
301
+ *
302
+ * "reasoning_effort must be one of: minimal, low, medium, high"
303
+ *
304
+ * 이 한 줄이면 짐작을 그만두고 사실로 갈아탈 수 있다.
305
+ *
306
+ * @returns {{무엇:string, 값:any, 왜:string}|null}
307
+ */
308
+ export function 배울전선(문구) {
309
+ const s = String(문구 ?? '');
310
+ if (!s) return null;
311
+ /*
312
+ * 「안 받는다」 를 뜻하는 말들. 창구마다 쓰는 낱말이 다르다.
313
+ *
314
+ * Anthropic "is deprecated" (아직 400 은 아닌데 곧 그렇게 된다)
315
+ * OpenAI "Unrecognized request argument supplied"
316
+ * Bedrock "ValidationException ... not supported"
317
+ *
318
+ * 하나라도 빠지면 그 창구에서만 못 배운다 — 그리고 못 배우면 같은 400 을
319
+ * 세션마다 다시 맞는다. 넓게 잡되, 아래에서 **칸 이름과 함께** 있을 때만
320
+ * 쓴다. 낱말 하나로는 아무 오류 문장에나 걸린다.
321
+ */
322
+ const 거절 = /not supported|unsupported|unrecognized|unknown|unexpected|invalid|is not permitted|not allowed|removed|deprecated|cannot be used|no longer|extra fields|additional propert/i;
323
+
324
+ /*
325
+ * ── 「그 칸을 안 받는다」 와 「그 값이 틀렸다」 는 다른 말이다 ─────────
326
+ *
327
+ * 이 자리가 여태 둘을 안 갈랐다. 그래서 이런 것들을 전부 「그 칸을 안
328
+ * 받는다」 로 읽었다.
329
+ *
330
+ * thinking.budget_tokens must be greater than or equal to 1024
331
+ * max_tokens must be greater than thinking.budget_tokens
332
+ * Expected `thinking` block to have a signature
333
+ *
334
+ * 셋 다 **값을 고치면 되는 말**이다. 그런데 「budget_tokens 를 안 받는구나」
335
+ * 로 읽고 생각형식을 adaptive 로 바꿨다. 그리고 그 카드를 **디스크에
336
+ * 남긴다.** budget 만 받는 모델은 그때부터 세션마다 400 이고, 사람이 배움
337
+ * 파일을 지우기 전에는 안 낫는다 — 되돌릴 길이 화면에 안 보이는 고장이다.
338
+ *
339
+ * 잘못 배우는 것은 못 배우는 것보다 나쁘다. 못 배우면 같은 400 을 다시
340
+ * 맞을 뿐이지만, 잘못 배우면 **멀쩡하던 것이 망가진 채로 굳는다.**
341
+ * 그래서 값 탓으로 읽히면 아무것도 안 배운다.
342
+ */
343
+ const 값탓 = /must be (?:greater|less|at least|at most|between|larger|smaller|higher|lower|a |an |of type)|(?:minimum|maximum|min_?tokens|max_?value)\b|out of range|expected [^\n]{0,40}to have|too (?:large|small|long|short|many|few)|>=|<=/i;
344
+
345
+ // ── 받는 값의 목록을 통째로 알려 주는 자리 ──────────────────────────
346
+ //
347
+ // 값 탓 중에서 **이것 하나만** 배울 것이 있다. 서버가 받는 값을 세어 주면
348
+ // 그건 짐작을 사실로 갈아탈 수 있는 유일한 문장이다. 그래서 값탓보다 먼저 본다.
349
+ const 목록 = /(?:reasoning_effort|effort)[^\n]{0,60}?(?:must be one of|one of|expected one of|valid values are)\s*[:\s]*([a-z0-9_,'"\s|-]+)/i.exec(s);
350
+ if (목록) {
351
+ // 쉼표로 세는 창구도 있고 띄어쓰기로만 세는 창구도 있다. 둘 다 받는다 —
352
+ // 한쪽만 보면 다른 쪽에서는 이 문장이 아래 「안 받는다」 로 굴러떨어진다.
353
+ const 값들 = 목록[1]
354
+ .split(/[,|\s]+/)
355
+ .map((x) => x.replace(/['"`.]/g, '').toLowerCase())
356
+ .filter((x) => /^[a-z]+$/.test(x) && x.length <= 12 && x !== 'or' && x !== 'and');
357
+ if (값들.length >= 2) return { 무엇: '눈금', 값: 값들, 왜: 짧게(s) };
358
+ }
359
+
360
+ // 값 탓이면 여기서 끝낸다. 칸을 끄는 것은 값을 고쳐서 될 일이 아니다.
361
+ if (값탓.test(s)) return null;
362
+
363
+ // ── 생각 칸 ─────────────────────────────────────────────────────────
364
+ if (/budget_tokens/i.test(s) && 거절.test(s)) {
365
+ return { 무엇: '생각형식', 값: 'adaptive', 왜: 짧게(s) };
366
+ }
367
+ if (/adaptive/i.test(s) && 거절.test(s)) {
368
+ return { 무엇: '생각형식', 값: 'budget', 왜: 짧게(s) };
369
+ }
370
+ if (/reasoning_effort/i.test(s) && 거절.test(s)) {
371
+ return { 무엇: '생각형식', 값: 'none', 왜: 짧게(s) };
372
+ }
373
+ if (/output_config/i.test(s) && 거절.test(s)) {
374
+ return { 무엇: '효력칸', 값: null, 왜: 짧게(s) };
375
+ }
376
+ if (/\bthinking\b/i.test(s) && 거절.test(s)) {
377
+ return { 무엇: '생각형식', 값: 'none', 왜: 짧게(s) };
378
+ }
379
+
380
+ // ── 캐시 표식 ───────────────────────────────────────────────────────
381
+ if (/cache_control|prompt_cache_key|cache_creation/i.test(s) && 거절.test(s)) {
382
+ return { 무엇: '캐시', 값: 'none', 왜: 짧게(s) };
383
+ }
384
+
385
+ // ── 세션 이름 ───────────────────────────────────────────────────────
386
+ // 'user' 는 흔한 낱말이라 **칸 이야기일 때만** 본다. 안 그러면 아무
387
+ // 오류 문장에나 걸려서 멀쩡한 칸을 꺼 버린다.
388
+ if (/(?:parameter|property|field|argument)[^\n]{0,24}['"`]?(?:user|metadata)['"`]?/i.test(s) && 거절.test(s)) {
389
+ return { 무엇: '세션자리', 값: null, 왜: 짧게(s) };
390
+ }
391
+ if (/['"`](?:user|metadata)['"`][^\n]{0,40}(?:not supported|unsupported|unknown|unexpected|invalid)/i.test(s)) {
392
+ return { 무엇: '세션자리', 값: null, 왜: 짧게(s) };
393
+ }
394
+
395
+ // 칸 이름을 점으로 이어 적는 창구. `metadata.user_id: unsupported field` 처럼
396
+ // 이름이 먼저 오고 까닭이 뒤에 온다 — 위 두 무늬는 그 차례를 못 잡는다.
397
+ if (/\b(?:metadata\.user_id|user_id)\b/i.test(s) && 거절.test(s)) {
398
+ return { 무엇: '세션자리', 값: null, 왜: 짧게(s) };
399
+ }
400
+
401
+ // ── 흘려받기 usage ──────────────────────────────────────────────────
402
+ if (/stream_options/i.test(s) && 거절.test(s)) {
403
+ return { 무엇: '스트림usage', 값: false, 왜: 짧게(s) };
404
+ }
405
+
406
+ return null;
407
+ }
408
+
409
+ /**
410
+ * 배운 것을 카드에 적는다. 새 카드를 돌려준다 — 원본은 안 건드린다.
411
+ *
412
+ * 생각형식을 끄면 눈금도 같이 뜻이 없어진다. 그런 딸린 자리는 여기서 한 번에
413
+ * 맞춘다 — 부르는 쪽마다 기억하게 두면 언젠가 한 곳이 빠진다.
414
+ */
415
+ export function 카드고치기(카드, 고침) {
416
+ if (!카드 || !고침) return 카드;
417
+ const 새 = { ...카드 };
418
+ 새[고침.무엇] = 고침.값;
419
+ // 배운 칸을 적어 둔다. 남길 때 **배운 것만** 남기려고 센다 (카드저장꼴).
420
+ const 배운것 = new Set(카드.배운칸 ?? []);
421
+ 배운것.add(고침.무엇);
422
+ if (고침.무엇 === '생각형식') {
423
+ /*
424
+ * 생각형식을 바꾸면 효력칸도 따라간다. 다만 **이미 안 받는다고 배운
425
+ * 칸은 도로 켜지 않는다.**
426
+ *
427
+ * 안 그러면 이렇게 된다 — 게이트웨이가 `output_config` 를 모른다고
428
+ * 400 을 냈고 우리는 그걸 배웠다. 그 뒤에 생각형식을 하나 더 배우는
429
+ * 순간 이 줄이 `output_config` 를 되살리고, 그 값이 「배운 것」 이라는
430
+ * 표까지 달고 디스크에 남는다. 서버가 안 받는다고 말해 준 칸을 우리가
431
+ * 배웠다고 적어 두는 셈이다.
432
+ */
433
+ const 효력끈적있나 = (카드.배운칸 ?? []).includes('효력칸') && 카드.효력칸 === null;
434
+ if (!효력끈적있나) {
435
+ 새.효력칸 = 고침.값 === 'adaptive' ? 'output_config' : null;
436
+ 배운것.add('효력칸');
437
+ }
438
+ if (고침.값 === 'none') { 새.눈금 = []; 배운것.add('눈금'); }
439
+ }
440
+ if (고침.무엇 === '눈금' && Array.isArray(고침.값)) {
441
+ // 서버가 알려 준 목록에 '끄는 말' 이 있으면 그것도 같이 배운다.
442
+ 새.끄는말 = 고침.값.find((x) => x === 'minimal' || x === 'none') ?? null;
443
+ 배운것.add('끄는말');
444
+ }
445
+ 새.배운칸 = [...배운것];
446
+ return 새;
447
+ }
448
+
449
+ export const 카드칸들 = ['생각형식', '눈금', '끄는말', '효력칸', '캐시', '세션자리', '스트림usage'];
450
+
451
+ /**
452
+ * 집 파일에 적을 만한 것만. 주소·모델은 부르는 쪽이 열쇠로 쓴다.
453
+ *
454
+ * ── 짐작은 안 남긴다. 배운 것만 남긴다 ─────────────────────────────────
455
+ *
456
+ * 여기는 카드를 통째로 남기고 있었다. 카드는 대부분 **짐작**이다(기본카드).
457
+ * 통째로 남기면 오늘의 짐작이 내일 「배운 것」 인 척 살아 돌아온다 —
458
+ * 짐작하는 자리를 고쳐도 남긴 것이 위에 얹혀서 그 고침이 영영 안 먹는다.
459
+ *
460
+ * 서버가 400 으로 알려 준 칸만 남긴다. 나머지는 켤 때마다 새로 짐작한다.
461
+ * 짐작이 나아지면 그날로 나아진다.
462
+ */
463
+ export function 카드저장꼴(카드) {
464
+ if (!카드) return null;
465
+ const 배운것 = Array.isArray(카드.배운칸) ? 카드.배운칸.filter((k) => 카드칸들.includes(k)) : [];
466
+ if (!배운것.length) return null;
467
+ const 남길것 = {};
468
+ for (const k of 배운것) 남길것[k] = 카드[k];
469
+ return 남길것;
470
+ }
471
+
472
+ /** 남겨 둔 것을 짐작 위에 얹는다. 없는 칸은 짐작 그대로 둔다. */
473
+ export function 카드합치기(기본, 남긴것) {
474
+ if (!남긴것 || typeof 남긴것 !== 'object') return 기본;
475
+ const 새 = { ...기본 };
476
+ const 배운것 = new Set(기본?.배운칸 ?? []);
477
+ for (const k of 카드칸들) {
478
+ if (남긴것[k] !== undefined) { 새[k] = 남긴것[k]; 배운것.add(k); }
479
+ }
480
+ // 이어서 또 배우면 앞서 배운 것과 함께 남는다.
481
+ 새.배운칸 = [...배운것];
482
+ return 새;
483
+ }
484
+
485
+ /**
486
+ * 연결에 전선 카드를 달아 준다. 켤 때 한 번 부른다.
487
+ *
488
+ * 요청마다 짓지 않는다 — 카드가 요청마다 달라지면 그것이 곧 몸통이 매번
489
+ * 달라진다는 뜻이고, 그러면 캐시가 영영 안 걸린다.
490
+ *
491
+ * @param {object} conn
492
+ * @param {object|null} 배움 agent/evolve.js 의 배움 (지난번에 배운 것)
493
+ */
494
+ export function 전선붙이기(conn, 배움 = null) {
495
+ if (!conn) return conn;
496
+ const 기본 = 기본카드(conn);
497
+ let 남긴것 = null;
498
+ try { 남긴것 = 배움?.아는전선?.(conn.model, conn.base, conn.kind) ?? null; } catch { 남긴것 = null; }
499
+ conn.전선 = 카드합치기(기본, 남긴것);
500
+ return conn;
501
+ }
502
+
503
+ /**
504
+ * 게이트웨이에 알려 줄 이 대화의 이름.
505
+ *
506
+ * **밖으로 나가는 값이다.** 그래서 담는 것은 대화 번호 하나뿐이다 — 경로도,
507
+ * 주소도, 사용자 이름도, 열쇠도 안 들어간다. 남이 봐도 아무것도 알 수
508
+ * 없어야 하고, 그러면서 같은 대화끼리는 같아야 한다.
509
+ *
510
+ * 글자도 좁게 자른다. 게이트웨이가 이 값을 로그 이름이나 주소에 쓰는 일이
511
+ * 있어서, 이상한 글자가 섞이면 그쪽에서 터진다.
512
+ */
513
+ export function 세션이름짓기(id) {
514
+ const 원래 = String(id ?? '');
515
+ if (!원래) return null;
516
+
517
+ /*
518
+ * ── 걸러내지 않고, 아니면 통째로 바꾼다 ─────────────────────────────
519
+ *
520
+ * 처음에는 못 쓰는 글자를 지우기만 했다. 그런데 지우기는 **남는 것이
521
+ * 무엇인지 안 보는 방식**이라, 이런 것이 그대로 나갔다.
522
+ *
523
+ * C:\Users\yunseok\Desktop\비밀폴더 → deel-CUsersyunseokDesktop
524
+ *
525
+ * 한글만 떨어지고 사람 이름과 폴더 이름은 그대로 남았다. 이 값은 게이트웨이
526
+ * 로그에 적히고 주소에 끼워 쓰이기도 한다 — 즉 밖으로 나간다.
527
+ *
528
+ * 그래서 규칙을 뒤집는다. **이미 안전한 모양일 때만 그대로 쓰고**, 한 글자라도
529
+ * 아니면 원래 글은 버리고 지문으로 바꾼다. 지문은 아무것도 안 알려 주면서
530
+ * 같은 값에는 늘 같다 — 이 자리에 필요한 성질이 정확히 그 둘이다.
531
+ *
532
+ * 대화 번호는 원래 이 모양이라(agent/store.js), 평소에는 지문까지 갈 일이 없다.
533
+ */
534
+ if (/^[A-Za-z0-9_-]{1,48}$/.test(원래)) return `deel-${원래}`;
535
+ return `deel-${지문(원래)}`;
536
+ }
537
+
538
+ /**
539
+ * 되돌릴 수 없는 짧은 지문 (FNV-1a 32비트를 씨앗 둘로, 16진수 16자).
540
+ *
541
+ * 밖으로 나가는 값이라 원래 글을 되찾을 수 없어야 하고, 그러면서 같은 대화는
542
+ * 늘 같은 값이어야 한다. 의존성 0개라 여기서 직접 짠다 — node:crypto 로도
543
+ * 되지만, 이 값은 비밀을 지키는 자물쇠가 아니라 **이름표**라 그만큼이면 된다.
544
+ */
545
+ function 지문(글) {
546
+ const 한판 = (씨) => {
547
+ let h = 씨 >>> 0;
548
+ for (let i = 0; i < 글.length; i++) {
549
+ h ^= 글.charCodeAt(i);
550
+ h = Math.imul(h, 0x01000193) >>> 0;
551
+ }
552
+ return h.toString(16).padStart(8, '0');
553
+ };
554
+ return 한판(0x811c9dc5) + 한판(0x9e3779b9);
555
+ }
556
+
557
+ /**
558
+ * 화면 한 줄. `/status` 와 `/think` 가 같은 것을 쓴다.
559
+ *
560
+ * 사람이 `/think max` 를 쳤을 때 **실제로 무엇이 나가는지** 보이는 자리다.
561
+ * 여태 화면과 전선이 다른 말을 하고 있었다.
562
+ *
563
+ * ── 이름표는 옮기고, 전선에 나가는 글자는 안 옮긴다 ────────────────────
564
+ *
565
+ * `adaptive` · `reasoning_effort` · `low·medium·high` 는 **몸에 그대로
566
+ * 실려 나가는 글자**다. 이걸 옮기면 화면에 적힌 말과 실제로 나가는 값이
567
+ * 달라진다 — 이 줄이 존재하는 이유가 바로 그 둘을 맞추는 것이라, 옮기면
568
+ * 줄이 스스로를 배반한다. 그래서 옮기는 것은 이름표(`생각` `눈금` `캐시`)와
569
+ * 우리가 지어낸 말(`표식` `서버자동`) 뿐이다.
570
+ */
571
+ export function 전선말(카드) {
572
+ if (!카드) return '';
573
+ const 조각 = [];
574
+ // 전선에 나가는 글자 그대로. 'none' 만 나갈 값이 없다는 뜻이라 옮긴다.
575
+ const 생각 = {
576
+ adaptive: 'adaptive', budget: 'budget_tokens', effort: 'reasoning_effort',
577
+ /*
578
+ * 나머지가 다 **나가는 칸 이름**인데 이것만 'on/off' 였다. 그런데 이
579
+ * 규격은 참·거짓만 받는 것이 아니라 눈금 글자도 받고, 실제로 눈금을 정해
580
+ * 두면 눈금이 나간다(adapter.js 의 buildBody). 화면은 「on/off」 라고 하고
581
+ * 전선에는 `think:"high"` 가 나가던 셈이다 — 이 모듈이 없애겠다고 만든
582
+ * 어긋남이다. 칸 이름으로 적고, 받는 눈금은 옆의 눈금 칸이 말해 준다.
583
+ */
584
+ boolean: 'think', none: 말('wire.none'),
585
+ }[카드.생각형식] ?? 카드.생각형식;
586
+ 조각.push(`${말('wire.think')} ${생각}`);
587
+ if (카드.눈금?.length) 조각.push(`${말('wire.rungs')} ${카드.눈금.join('·')}`);
588
+ const 캐시 = {
589
+ explicit: 말('wire.cacheMarked'),
590
+ key: 말('wire.cacheKey'),
591
+ auto: 말('wire.cacheAuto'),
592
+ none: 말('wire.none'),
593
+ }[카드.캐시] ?? 카드.캐시;
594
+ 조각.push(`${말('wire.cache')} ${캐시}`);
595
+ return 조각.join(' · ');
596
+ }
597
+
598
+ function 짧게(s) {
599
+ return String(s).replace(/\s+/g, ' ').trim().slice(0, 200);
600
+ }