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,352 @@
1
+ // 도구 목록을 **보내기 직전에** 그 회사 규격에 맞춘다.
2
+ //
3
+ // ── 왜 여기서 하나 ──────────────────────────────────────────────────────
4
+ //
5
+ // 도구 목록을 만드는 자리(tools/index.js)는 어디로 보낼지 모른다. 같은 목록이
6
+ // 로컬 llama.cpp 로도 가고 Anthropic 으로도 간다. 그래서 만드는 자리에서
7
+ // 「Gemini 가 싫어하는 열쇠」 를 빼면, 그것을 아무렇지 않게 받는 서버에서도
8
+ // 값을 잃는다.
9
+ //
10
+ // 반대로 보내는 자리(adapter.js 의 chat·chatStream)는 conn 을 쥐고 있다.
11
+ // 주소를 보면 어디로 가는지 안다. 그러니 **거기서 한 번** 다듬는 것이 맞다.
12
+ //
13
+ // ── 어디로 가는지는 주소로 안다 ─────────────────────────────────────────
14
+ //
15
+ // 모델 이름으로 짐작하지 않는다. LiteLLM 같은 사내 게이트웨이 뒤에 Claude 가
16
+ // 있어도, 우리가 말을 거는 상대는 게이트웨이다. 그 게이트웨이가 제 나름대로
17
+ // 이미 다듬어서 넘긴다 — 우리가 모델 이름만 보고 「Anthropic 이니까」 하고
18
+ // 깎으면, 게이트웨이는 멀쩡히 받던 것을 못 받게 된다.
19
+ //
20
+ // 그래서 **주소의 호스트**만 본다. 모르는 주소면 아무것도 안 건드린다.
21
+ // 이 프로그램의 주된 자리(로컬 모델·사내 게이트웨이)가 바로 그 「모르는
22
+ // 주소」 라, 기본은 지금까지와 똑같이 도는 쪽이어야 한다.
23
+ //
24
+ // ── 이름을 바꾸면 되돌려야 한다 ─────────────────────────────────────────
25
+ //
26
+ // 이름이 규격에 안 맞아 고쳐 보내면, 모델은 **고친 이름**으로 부른다.
27
+ // 그대로 위로 올리면 loop.js 가 모르는 도구를 받는다 — 도구가 조용히
28
+ // 안 불리는, 제일 알아내기 어려운 고장이다. 그래서 다듬은 자리에서
29
+ // 되돌림 표를 같이 내놓고, 답이 오면 adapter.js 가 그 표로 되돌린다.
30
+ // 밖에서는 이 일이 있었는지조차 모른다.
31
+ import { 애저인가 } from './azure.js';
32
+
33
+ /**
34
+ * 이름 규칙.
35
+ *
36
+ * OpenAI 문서에 적힌 그대로다 — "alphanumeric characters, underscores, or
37
+ * dashes, with a maximum length of 64 characters". Anthropic·Bedrock 도 같은
38
+ * 모양을 쓰고, Gemini 는 여기에 점을 더 받는다. 제일 좁은 것 하나로 맞추면
39
+ * 어디로 보내도 통한다 — 회사마다 다르게 깎을 까닭이 없다.
40
+ */
41
+ const 이름규칙 = /^[a-zA-Z0-9_-]{1,64}$/;
42
+ const 이름최대 = 64;
43
+
44
+ /**
45
+ * Gemini 가 받는 스키마 열쇠.
46
+ *
47
+ * 문서가 "function calling supports only a subset of the OpenAPI schema" 라고
48
+ * 말한다. 무엇이 되는지를 적어 둔 목록은 있어도 **무엇이 안 되는지**를 적어
49
+ * 둔 목록은 없다. 그러니 허용 목록으로 간다 — 모르는 열쇠를 실어 보내는 쪽이
50
+ * 위험하다. 안 되는 열쇠 하나가 400 을 만들고, 그러면 그 턴이 통째로 죽는다.
51
+ *
52
+ * 우리 도구는 이 목록 안에서만 쓴다(type·description·properties·required·
53
+ * items·enum). 그래서 이 거르개에 걸리는 것은 사실상 **MCP 서버가 준 스키마**
54
+ * 뿐이다. 남이 만든 스키마에는 $schema·$ref·oneOf·additionalProperties 가
55
+ * 흔하다.
56
+ */
57
+ const 제미니열쇠 = new Set([
58
+ 'type', 'format', 'description', 'nullable', 'enum', 'items', 'properties',
59
+ 'required', 'minItems', 'maxItems', 'minimum', 'maximum', 'anyOf', 'title',
60
+ 'example', 'default', 'propertyOrdering',
61
+ ]);
62
+
63
+ /**
64
+ * format 은 값까지 본다.
65
+ *
66
+ * 열쇠는 받는데 모르는 **값**이면 거절하는 자리다. `format: 'uri'` ·
67
+ * `'uuid'` 같은 것이 MCP 스키마에 자주 있다. 지우면 뜻이 조금 얕아질 뿐이고,
68
+ * 실어 보내면 요청이 죽는다 — 어느 쪽이 나은지는 분명하다.
69
+ */
70
+ const 제미니format = {
71
+ string: new Set(['date-time', 'date', 'time', 'duration', 'enum']),
72
+ number: new Set(['float', 'double']),
73
+ integer: new Set(['int32', 'int64']),
74
+ };
75
+
76
+ /**
77
+ * 이 주소는 어느 회사인가.
78
+ *
79
+ * @returns {'anthropic'|'gemini'|'bedrock'|'openai'|'azure'|null} 모르면 null —
80
+ * 그때는 **아무것도 안 건드린다**.
81
+ *
82
+ * ── Azure 를 왜 따로 부르나 ─────────────────────────────────────────────
83
+ *
84
+ * 도구를 다듬는 데는 둘이 똑같다(아래 도구맞추기 에서 둘 다 이름만 손본다).
85
+ * 갈리는 자리는 **몸통**이다 — OpenAI 직통의 추론 모델은 출력 상한을 옛 이름
86
+ * (`max_tokens`)으로 주면 「지원 안 하는 인자」 라고 튕기는데, Azure 는 옛 판이
87
+ * 아직 많아서 그 이름을 그대로 받는다. 둘을 한 이름으로 묶어 두면 한쪽을
88
+ * 고치는 순간 다른 쪽이 깨진다.
89
+ *
90
+ * Azure 인지는 **azure.js 에게 묻는다.** 여기서 글자 조각으로 따로 판단하면
91
+ * detect 와 답이 갈리고, 갈리는 순간 한쪽만 맞는 자리가 생긴다.
92
+ */
93
+ /**
94
+ * mantle 창구의 호스트인가.
95
+ *
96
+ * 이 규칙이 여기와 backend/detect.js 두 군데에 똑같이 적혀 있었다. AWS 가
97
+ * 이름을 하나 더 내면 한쪽만 고쳐지고, 그러면 「연결은 되는데 전선 카드가
98
+ * 모르는 주소로 선다」 는 반쯤 되는 상태가 된다 — 화면에 아무 말도 안 뜨는
99
+ * 종류다. 주소를 알아보는 규칙은 이 파일이 갖는다고 이미 정해 두었으므로
100
+ * (아래 벤더 머리말) 여기에 두고 detect 가 가져다 쓴다.
101
+ *
102
+ * @param {string} 호스트 소문자로 맞춘 hostname
103
+ */
104
+ export function 맨틀호스트인가(호스트) {
105
+ const h = String(호스트 ?? '').toLowerCase();
106
+ return h.startsWith('bedrock') && h.endsWith('.api.aws');
107
+ }
108
+
109
+ export function 벤더(conn) {
110
+ let 호스트 = '';
111
+ try { 호스트 = new URL(String(conn?.base ?? '')).hostname.toLowerCase(); }
112
+ catch { 호스트 = ''; }
113
+
114
+ if (호스트) {
115
+ if (호스트 === 'anthropic.com' || 호스트.endsWith('.anthropic.com')) return 'anthropic';
116
+ if (호스트.endsWith('.googleapis.com')) return 'gemini';
117
+ // bedrock-runtime.<리전>.amazonaws.com. amazonaws 아래에는 남의 것도 많아서
118
+ // 앞머리까지 본다 — S3 주소를 Bedrock 으로 읽으면 안 된다.
119
+ if (호스트.startsWith('bedrock') && 호스트.endsWith('.amazonaws.com')) return 'bedrock';
120
+ /*
121
+ * mantle 은 amazonaws.com 이 아니라 **api.aws** 아래에 있다.
122
+ *
123
+ * bedrock-mantle.us-east-1.api.aws
124
+ *
125
+ * 위 줄만 있으면 이 주소가 Bedrock 으로 안 읽힌다. 그러면 전선 카드가
126
+ * 회사를 못 정하고(backend/wire.js), 눈금도 캐시 최소 크기도 전부
127
+ * 「모르는 주소」 취급이 된다 — 즉 mantle 로 붙인 사람만 조용히 손해를
128
+ * 본다. 여기서도 앞머리를 같이 보는 이유는 위와 같다.
129
+ */
130
+ if (맨틀호스트인가(호스트)) return 'bedrock';
131
+ if (애저인가(conn?.base)) return 'azure';
132
+ if (호스트 === 'api.openai.com') return 'openai';
133
+ }
134
+ /*
135
+ * 주소를 못 읽었을 때만 규격을 본다.
136
+ *
137
+ * 규격이 'anthropic' 이라는 것은 우리가 Anthropic **말투**로 말한다는 뜻이라,
138
+ * 상대가 그 규칙을 볼 확률이 높다. 주소가 있는데도 규격을 앞세우면, 사내
139
+ * 게이트웨이를 Anthropic 말투로 붙여 쓰는 사람이 손해를 본다.
140
+ */
141
+ if (!호스트 && conn?.kind === 'anthropic') return 'anthropic';
142
+ return null;
143
+ }
144
+
145
+ /** 규격에 맞는 이름으로. 못 쓰는 글자는 밑줄, 너무 길면 가운데를 지문으로 접는다. */
146
+ function 이름다듬기(원래, 쓴것) {
147
+ /*
148
+ * 못 쓰는 글자는 **덩어리째** 밑줄 하나로 바꾸고, 지문을 붙인다.
149
+ *
150
+ * 글자마다 밑줄로 바꾸면 이렇게 된다 —
151
+ *
152
+ * mcp__사내문서__검색 → mcp__________
153
+ * mcp__사내문서__열기 → mcp__________
154
+ * mcp__사내문서__지우기 → mcp___________
155
+ *
156
+ * 겹치는 것은 뒤에 번호를 붙여 면할 수 있다. 그런데 **모델이 보는 이름이
157
+ * 서로 구별이 안 된다.** 「검색해 줘」 라고 했는데 지우기가 불릴 수 있고,
158
+ * 그건 오류도 안 난다 — 이 파일이 없애려던 바로 그 고장이다.
159
+ *
160
+ * 그래서 지문을 붙인다. 읽히지는 않아도 **서로 다르고, 판마다 같다.**
161
+ * 무엇을 하는 도구인지는 설명이 그대로 지고 간다(`[사내문서] 검색`) —
162
+ * 이름이 못 읽히는 자리에서 뜻을 지는 것은 설명 쪽이다.
163
+ */
164
+ const 원본 = String(원래 ?? '');
165
+ let 새것 = 원본.replace(/[^a-zA-Z0-9_-]+/g, '_');
166
+ if (새것 !== 원본) 새것 = `${새것.replace(/_+$/, '')}_${지문(원본)}`;
167
+ if (!새것 || !/[a-zA-Z0-9]/.test(새것)) 새것 = `tool_${지문(원본)}`;
168
+ if (새것.length > 이름최대) {
169
+ /*
170
+ * 앞뒤를 남기고 가운데를 지문으로 접는다.
171
+ *
172
+ * 앞만 남기고 자르면 안 된다. `mcp__아주긴서버이름__read` 와 `..__write`
173
+ * 가 앞 64자에서 **똑같아진다**. 우리 쪽은 뒤에 번호를 붙여 겹침을 면할
174
+ * 수 있지만, 모델이 보는 이름은 둘 다 `mcp__아주긴서버이름…` 이라
175
+ * 무엇이 읽기고 무엇이 쓰기인지 알 길이 없다. 그러면 모델은 아무거나
176
+ * 고르고, 우리는 그것을 오류로도 못 잡는다.
177
+ *
178
+ * 도구 이름에서 뜻을 지고 있는 자리는 **뒤쪽**(도구 이름)이고, 앞쪽은
179
+ * 어느 서버인지를 말한다. 둘 다 남겨야 이름이 이름 노릇을 한다.
180
+ * 가운데를 지문으로 채우는 것은 그러고도 겹치지 않게 하려는 것이다.
181
+ */
182
+ const 지문6 = 지문(원래);
183
+ const 앞몫 = 26;
184
+ const 뒷몫 = 이름최대 - 앞몫 - 지문6.length - 2; // 밑줄 둘
185
+ 새것 = `${새것.slice(0, 앞몫)}_${지문6}_${새것.slice(-뒷몫)}`;
186
+ }
187
+ // 그래도 겹치면 뒤에 숫자를 붙인다. 겹치는 일은 드물지만, 나면 조용하다.
188
+ if (쓴것.has(새것)) {
189
+ let n = 2;
190
+ let 후보 = '';
191
+ do { 후보 = 새것.slice(0, 이름최대 - String(n).length - 1) + '_' + n; n++; } while (쓴것.has(후보));
192
+ 새것 = 후보;
193
+ }
194
+ return 새것;
195
+ }
196
+
197
+ /**
198
+ * 짧은 지문. 암호용이 아니라 이름이 안 겹치게 하려는 것뿐이다.
199
+ *
200
+ * **뒷자리를 남긴다.** 앞자리를 남기면 안 된다 — 32비트 값을 36진수로 적으면
201
+ * 길어야 일곱 자라, 여덟 자로 맞추려고 앞을 0 으로 채우게 된다. 그 상태에서
202
+ * 앞 여섯 자를 떼면 채워 넣은 0 을 세고 있는 셈이고, 정작 제일 자주 바뀌는
203
+ * 뒷자리 둘이 통째로 날아간다. 21억 가지로 갈리라고 만든 것이 330만 가지로
204
+ * 줄었다(이름 20만 개로 재 보니 겹침이 4건에서 5,825건으로 늘었다).
205
+ *
206
+ * 겹치면 아래에서 뒤에 번호를 붙여 면하는데, 그 번호는 **목록 차례를 탄다.**
207
+ * 그러니 지문이 겹치는 순간 이 파일이 없애려던 고장이 그대로 돌아온다 —
208
+ * MCP 서버가 다시 붙어 도구 차례가 바뀌면 검색과 지우기가 이름을 맞바꾼다.
209
+ */
210
+ function 지문(글, 자릿수 = 6) {
211
+ let h = 2166136261;
212
+ const s = String(글);
213
+ for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
214
+ return (h >>> 0).toString(36).padStart(자릿수, '0').slice(-자릿수);
215
+ }
216
+
217
+ /**
218
+ * Gemini 가 받는 모양으로 스키마를 다시 짓는다.
219
+ *
220
+ * 못 받는 열쇠를 빼고 나면 `type` 이 없어지는 마디가 생긴다($ref 만 있던 곳).
221
+ * 그대로 두면 그 마디가 뜻 없는 빈 것이 되므로, 남은 것을 보고 갈래를
222
+ * 되짚어 준다. 되짚을 것조차 없으면 string 으로 둔다 — 없는 것보다 낫다.
223
+ */
224
+ function 제미니스키마(값) {
225
+ if (Array.isArray(값)) return 값.map(제미니스키마);
226
+ if (!값 || typeof 값 !== 'object') return 값;
227
+
228
+ const 새것 = {};
229
+ for (const [k, v] of Object.entries(값)) {
230
+ if (!제미니열쇠.has(k)) continue;
231
+ if (k === 'properties' && v && typeof v === 'object') {
232
+ const 속성 = {};
233
+ for (const [n, s] of Object.entries(v)) 속성[n] = 제미니스키마(s);
234
+ 새것.properties = 속성;
235
+ continue;
236
+ }
237
+ if (k === 'items' || k === 'anyOf') { 새것[k] = 제미니스키마(v); continue; }
238
+ 새것[k] = v;
239
+ }
240
+
241
+ /*
242
+ * `type: ['string','null']` 은 JSON Schema 의 말이지 OpenAPI 의 말이 아니다.
243
+ * 널을 받는다는 뜻이었으니 nullable 로 옮겨 적는다 — 그냥 버리면 "널을
244
+ * 줘도 된다" 는 정보가 사라진다.
245
+ */
246
+ if (Array.isArray(새것.type)) {
247
+ const 진짜 = 새것.type.filter((t) => t !== 'null');
248
+ if (새것.type.length !== 진짜.length) 새것.nullable = true;
249
+ 새것.type = 진짜[0] ?? 'string';
250
+ }
251
+
252
+ if (새것.format !== undefined) {
253
+ const 받는것 = 제미니format[String(새것.type)];
254
+ if (!받는것 || !받는것.has(String(새것.format))) delete 새것.format;
255
+ }
256
+
257
+ if (새것.type === undefined) {
258
+ if (새것.properties) 새것.type = 'object';
259
+ else if (새것.items) 새것.type = 'array';
260
+ else if (새것.anyOf) { /* anyOf 는 갈래를 안 적어도 된다 */ }
261
+ else 새것.type = 'string';
262
+ }
263
+ return 새것;
264
+ }
265
+
266
+ /** 규격이 요구하는 「인자는 객체」 를 지킨다. 빈 것도 객체여야 한다. */
267
+ function 객체로(p) {
268
+ if (p && typeof p === 'object' && !Array.isArray(p) && p.type === 'object') return p;
269
+ if (p && typeof p === 'object' && !Array.isArray(p) && p.properties) return { ...p, type: 'object' };
270
+ return { type: 'object', properties: {} };
271
+ }
272
+
273
+ /**
274
+ * 보내기 직전 다듬기.
275
+ *
276
+ * @param {Array|null} tools toolSchemas() 가 낸 OpenAI 모양 목록
277
+ * @param {object} conn
278
+ * @returns {{tools: Array|null, 되돌림: Map<string,string>|null, 손본것: {이름: number, 스키마: number}}}
279
+ *
280
+ * **되돌림이 null 이면 이름을 하나도 안 바꿨다는 뜻이다.** 부르는 쪽이 그
281
+ * 경우에 아무 일도 안 하도록, 빈 Map 대신 null 을 준다.
282
+ */
283
+ export function 도구맞추기(tools, conn) {
284
+ const 그대로 = { tools, 되돌림: null, 손본것: { 이름: 0, 스키마: 0 } };
285
+ if (!Array.isArray(tools) || !tools.length) return 그대로;
286
+ const v = 벤더(conn);
287
+ if (!v) return 그대로;
288
+
289
+ const 되돌림 = new Map();
290
+ const 쓴것 = new Set();
291
+ let 이름손봄 = 0;
292
+ let 스키마손봄 = 0;
293
+
294
+ const 새목록 = tools.map((t) => {
295
+ const f = t?.function ?? t;
296
+ const 원래이름 = String(f?.name ?? '');
297
+ let 이름 = 원래이름;
298
+ if (!이름규칙.test(이름)) {
299
+ 이름 = 이름다듬기(원래이름, 쓴것);
300
+ 되돌림.set(이름, 원래이름);
301
+ 이름손봄++;
302
+ }
303
+ 쓴것.add(이름);
304
+
305
+ let 인자 = f?.parameters ?? f?.input_schema ?? null;
306
+ if (v === 'gemini') {
307
+ const 다듬은것 = 객체로(제미니스키마(인자));
308
+ if (JSON.stringify(다듬은것) !== JSON.stringify(인자)) 스키마손봄++;
309
+ 인자 = 다듬은것;
310
+ } else if (v === 'anthropic' || v === 'bedrock') {
311
+ // 이 둘은 「인자는 객체」 를 규격으로 못 박는다. 나머지 열쇠는 안 건드린다 —
312
+ // 확인 못 한 것을 깎으면, 멀쩡히 쓰던 MCP 도구의 뜻이 조용히 얕아진다.
313
+ const 다듬은것 = 객체로(인자);
314
+ if (다듬은것 !== 인자) 스키마손봄++;
315
+ 인자 = 다듬은것;
316
+ }
317
+
318
+ if (이름 === 원래이름 && 인자 === (f?.parameters ?? f?.input_schema ?? null)) return t;
319
+ // 원래 없던 인자 칸을 만들어 두지 않는다. 이름만 고친 도구에 빈 칸이
320
+ // 새로 생기면, 그 자리가 무엇이었는지 밖에서 알 길이 없어진다.
321
+ const 새함수 = { ...f, name: 이름 };
322
+ if (인자 != null) 새함수.parameters = 인자;
323
+ return { ...t, function: 새함수 };
324
+ });
325
+
326
+ return {
327
+ tools: 새목록,
328
+ 되돌림: 되돌림.size ? 되돌림 : null,
329
+ 손본것: { 이름: 이름손봄, 스키마: 스키마손봄 },
330
+ };
331
+ }
332
+
333
+ /**
334
+ * 답에 실린 도구 이름을 원래 것으로 되돌린다.
335
+ *
336
+ * 안 바꾼 이름은 표에 없다 — 그때는 그대로 둔다. 표에 없다고 지우거나
337
+ * 비우면, 다듬을 일이 없던 도구까지 못 부르게 된다.
338
+ */
339
+ export function 이름되돌리기(message, 되돌림) {
340
+ if (!되돌림 || !message?.toolCalls?.length) return message;
341
+ let 바꾼적있나 = false;
342
+ const 새것 = message.toolCalls.map((tc) => {
343
+ const 이름 = tc?.function?.name ?? tc?.name;
344
+ const 원래 = 되돌림.get(String(이름 ?? ''));
345
+ if (원래 === undefined) return tc;
346
+ 바꾼적있나 = true;
347
+ return tc.function
348
+ ? { ...tc, function: { ...tc.function, name: 원래 } }
349
+ : { ...tc, name: 원래 };
350
+ });
351
+ return 바꾼적있나 ? { ...message, toolCalls: 새것 } : message;
352
+ }