deel-local-cli 1.12.0 → 1.13.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.ko.md +6 -5
- package/README.md +6 -5
- package/package.json +60 -60
- package/src/acp/map.js +30 -25
- package/src/acp/serve.js +810 -810
- package/src/agent/budget.js +156 -156
- package/src/agent/commit.js +32 -9
- package/src/agent/compact.js +441 -420
- package/src/agent/effort.js +308 -308
- package/src/agent/evolve.js +263 -263
- package/src/agent/loop.js +1728 -1707
- package/src/agent/memory.js +156 -156
- package/src/agent/mention.js +210 -210
- package/src/agent/recall.js +22 -9
- package/src/agent/session.js +1056 -1049
- package/src/backend/adapter.js +1355 -1159
- package/src/backend/cachemark.js +223 -150
- package/src/backend/detect.js +327 -327
- package/src/backend/http.js +3 -1
- package/src/backend/mcp.js +15 -3
- package/src/backend/price.js +69 -6
- package/src/backend/probe.js +17 -3
- package/src/backend/quota.js +274 -250
- package/src/backend/tokens.js +25 -3
- package/src/backend/toolfit.js +352 -352
- package/src/backend/wire.js +715 -600
- package/src/commands.js +3184 -3135
- package/src/config.js +39 -0
- package/src/i18n/en.js +545 -544
- package/src/i18n/ja.js +498 -497
- package/src/i18n/ko.js +592 -591
- package/src/i18n/zh.js +498 -497
- package/src/oneshot.js +625 -620
- package/src/pack/tar.js +23 -1
- package/src/pack/zip.js +19 -1
- package/src/plugins/manage.js +85 -5
- package/src/repl.js +2616 -2602
- package/src/safety/audit.js +22 -3
- package/src/safety/guard.js +124 -9
- package/src/safety/network.js +43 -0
- package/src/tools/fastgrep.js +15 -0
- package/src/tools/fsutil.js +291 -265
- package/src/tools/index.js +40 -3
- package/src/tools/jobs.js +87 -6
- package/src/tools/label.js +52 -0
- package/src/tools/spawn.js +114 -0
- package/src/tools/verify.js +358 -358
- package/src/tools/webfetch.js +28 -3
- package/src/ui/export.js +26 -10
- package/src/ui/pick.js +115 -115
- package/src/ui/status.js +617 -613
package/src/agent/loop.js
CHANGED
|
@@ -1,1707 +1,1728 @@
|
|
|
1
|
-
// 에이전트 루프. 모델 → 도구 → 결과 → 모델 을 답이 나올 때까지 돈다.
|
|
2
|
-
// 화면에 그릴 것은 이벤트로 흘려보낸다 — 화면 코드와 섞지 않는다.
|
|
3
|
-
import { chat, chatStream, assistantMessage, toolMessage, 말없이끝남, 보낸토큰,
|
|
4
|
-
import { 그림메시지 } from '../backend/vision.js';
|
|
5
|
-
import { 어떻게할까 } from '../safety/policy.js';
|
|
6
|
-
import { toolSchemas, runTool, TOOLS, 파일현황 } from '../tools/index.js';
|
|
7
|
-
import { isMutating } from '../safety/guard.js';
|
|
8
|
-
import { effortFor, tokensFor, fullCap, wasCut, shiftLevel, 자동강도, 인사인가 as 인사말인가 } from './effort.js';
|
|
9
|
-
import { 배울전선, 카드고치기, 카드저장꼴, 전선붙이기 } from '../backend/wire.js';
|
|
10
|
-
import { 살린쓰기 } from './salvage.js';
|
|
11
|
-
import { 배울것, 길이문제인가 } from '../backend/learn.js';
|
|
12
|
-
import { compact, shouldCompact, shouldFold, foldToolResults, foldImages, 못박을것, 접힌파일열쇠 } from './compact.js';
|
|
13
|
-
import { 걸음수, 하위걸음수, 요약길이 } from './budget.js';
|
|
14
|
-
import { Session } from './session.js';
|
|
15
|
-
import { 최대깊이, 하위모드, 하위요약 } from '../tools/task.js';
|
|
16
|
-
import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './models.js';
|
|
17
|
-
import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
18
|
-
import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴까 } from '../safety/secrets.js';
|
|
19
|
-
import { 바깥인가 } from '../safety/runmode.js';
|
|
20
|
-
import { get as workMode } from './modes.js';
|
|
21
|
-
import { 지시말 } from '../i18n/index.js';
|
|
22
|
-
import { 빠진것, 빠졌다는말 } from './asks.js';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
깨우기 =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
막힘.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* `Read(PDF_
|
|
294
|
-
*
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* 하위가
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
*
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
*
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
+ '
|
|
523
|
-
+ '
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
*
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
let
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
*
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
*
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
+ '
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
conn.
|
|
869
|
-
if (
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
session.messages
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
*
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
*
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
//
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
:
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
/*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
* 안
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
session.usage.
|
|
1430
|
-
session.usage.
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
session.
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if (
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
if (call.name === '
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
//
|
|
1526
|
-
|
|
1527
|
-
const
|
|
1528
|
-
/*
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
*/
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
:
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1
|
+
// 에이전트 루프. 모델 → 도구 → 결과 → 모델 을 답이 나올 때까지 돈다.
|
|
2
|
+
// 화면에 그릴 것은 이벤트로 흘려보낸다 — 화면 코드와 섞지 않는다.
|
|
3
|
+
import { chat, chatStream, assistantMessage, toolMessage, 말없이끝남, 보낸토큰, 부른것들, 도구결과인가 } from '../backend/adapter.js';
|
|
4
|
+
import { 그림메시지 } from '../backend/vision.js';
|
|
5
|
+
import { 어떻게할까 } from '../safety/policy.js';
|
|
6
|
+
import { toolSchemas, runTool, TOOLS, 파일현황 } from '../tools/index.js';
|
|
7
|
+
import { isMutating } from '../safety/guard.js';
|
|
8
|
+
import { effortFor, tokensFor, fullCap, wasCut, shiftLevel, 자동강도, 인사인가 as 인사말인가 } from './effort.js';
|
|
9
|
+
import { 배울전선, 카드고치기, 카드저장꼴, 전선붙이기 } from '../backend/wire.js';
|
|
10
|
+
import { 살린쓰기 } from './salvage.js';
|
|
11
|
+
import { 배울것, 길이문제인가 } from '../backend/learn.js';
|
|
12
|
+
import { compact, shouldCompact, shouldFold, foldToolResults, foldImages, 못박을것, 접힌파일열쇠 } from './compact.js';
|
|
13
|
+
import { 걸음수, 하위걸음수, 요약길이 } from './budget.js';
|
|
14
|
+
import { Session } from './session.js';
|
|
15
|
+
import { 최대깊이, 하위모드, 하위요약 } from '../tools/task.js';
|
|
16
|
+
import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './models.js';
|
|
17
|
+
import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
18
|
+
import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴까 } from '../safety/secrets.js';
|
|
19
|
+
import { 바깥인가 } from '../safety/runmode.js';
|
|
20
|
+
import { get as workMode } from './modes.js';
|
|
21
|
+
import { 지시말 } from '../i18n/index.js';
|
|
22
|
+
import { 빠진것, 빠졌다는말 } from './asks.js';
|
|
23
|
+
import { 환경속열쇠들 } from '../config.js';
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* 콜백으로만 소식을 주는 부름을, 제너레이터가 중간에 내보낼 수 있는 모양으로 바꾼다.
|
|
27
|
+
*
|
|
28
|
+
* chat() 이나 compact() 는 await 하나로 끝나는 부름이라, 그 안에서 "서버가 막았다,
|
|
29
|
+
* 2초 기다린다" 같은 소식이 생겨도 화면으로 나올 길이 없었다. 소식을 우편함에
|
|
30
|
+
* 넣어 두고 받는 쪽이 하나씩 꺼내 yield 한다. 부름이 끝나면 소식()도 끝난다.
|
|
31
|
+
*
|
|
32
|
+
* const 편지 = 우편함((알려줘) => chat(conn, { ..., onBackoff: 알려줘 }));
|
|
33
|
+
* for await (const 소식 of 편지.소식()) yield 소식;
|
|
34
|
+
* const 답 = await 편지.부름;
|
|
35
|
+
*/
|
|
36
|
+
function 우편함(부르기) {
|
|
37
|
+
const 함 = [];
|
|
38
|
+
let 깨우기 = null;
|
|
39
|
+
let 끝 = false;
|
|
40
|
+
const 부름 = 부르기((소식) => { 함.push(소식); 깨우기?.(); });
|
|
41
|
+
부름.then(() => { 끝 = true; 깨우기?.(); }, () => { 끝 = true; 깨우기?.(); });
|
|
42
|
+
return {
|
|
43
|
+
부름,
|
|
44
|
+
async *소식() {
|
|
45
|
+
while (!끝 || 함.length) {
|
|
46
|
+
if (함.length) { yield 함.shift(); continue; }
|
|
47
|
+
await new Promise((r) => { 깨우기 = r; });
|
|
48
|
+
깨우기 = null;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 아무 내용도 없는 답인가.
|
|
56
|
+
*
|
|
57
|
+
* 글도 없고 도구 호출도 없으면 아무 일도 안 일어난 것이다. 이걸 '성공' 으로
|
|
58
|
+
* 넘기면 화면에 걸린 시간만 찍히고 끝난다 — 오류도 없으니 사용자는 무엇이
|
|
59
|
+
* 잘못됐는지 알 길이 없다. 생각(thinking)만 있는 것도 마찬가지다.
|
|
60
|
+
*/
|
|
61
|
+
function 빈답인가(msg) {
|
|
62
|
+
if (!msg) return true;
|
|
63
|
+
if ((msg.toolCalls ?? []).length) return false;
|
|
64
|
+
return !String(msg.content ?? '').trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 도구 결과 중에서 **대화에 실을 글**을 고른다.
|
|
69
|
+
*
|
|
70
|
+
* ── 왜 따로 두나 ────────────────────────────────────────────────────────
|
|
71
|
+
*
|
|
72
|
+
* 여태 여기는 `result.content ?? ''` 한 줄이었다. 그런데 도구가 `summary` 만
|
|
73
|
+
* 돌려주는 자리가 있다 — Recall 이 지난 대화를 다 뒤졌지만 못 찾았을 때가
|
|
74
|
+
* 그렇다. 그 경우 모델에게 가는 것은 **빈 글**이다.
|
|
75
|
+
*
|
|
76
|
+
* 사람 화면에는 「지난 대화 12개를 다 뒤졌지만 없습니다」 가 멀쩡히 찍힌다.
|
|
77
|
+
* 그래서 아무도 눈치를 못 챈다. 정작 모델은 아무 말도 못 들었으므로,
|
|
78
|
+
* 못 찾았다는 것도 모르고 **찾아본 적이 있다는 것조차** 모른다. 그 자리에서
|
|
79
|
+
* 모델이 하는 일은 둘 중 하나다 — 없는 것을 지어내거나, 사람에게 엉뚱한
|
|
80
|
+
* 선택지를 들이민다. 실제 제보가 딱 그 모양이었다.
|
|
81
|
+
*
|
|
82
|
+
* 그래서 규칙을 셋으로 못 박는다.
|
|
83
|
+
* 1) 글이 있으면 그대로.
|
|
84
|
+
* 2) 글이 없고 요약이 있으면 요약을 싣는다 — 사람이 본 것과 같은 말이다.
|
|
85
|
+
* 3) 둘 다 없으면 **없다고 말한다.** 침묵은 지어내기를 부른다.
|
|
86
|
+
*/
|
|
87
|
+
export function 실을글(result) {
|
|
88
|
+
if (result?.error) return `오류: ${result.error}`;
|
|
89
|
+
const 글 = result?.content;
|
|
90
|
+
if (String(글 ?? '').trim()) return String(글);
|
|
91
|
+
const 요약 = String(result?.summary ?? '').trim();
|
|
92
|
+
if (요약) return 요약;
|
|
93
|
+
return '도구는 돌았지만 돌려준 것이 없습니다.'
|
|
94
|
+
+ ' 없는 것을 있다고 지어내지 말고, 못 얻었다는 것을 그대로 두고 다음으로 넘어가세요.';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// think 값을 규격에 맞게. 'off' 는 사고를 끈다.
|
|
98
|
+
function thinkFor(conn, level) {
|
|
99
|
+
if (level === 'off') return conn.kind === 'ollama' ? false : undefined;
|
|
100
|
+
return level;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 아무것도 안 바꾸는 도구들. 이것들만 동시에 돌린다.
|
|
104
|
+
//
|
|
105
|
+
// 왜 이것만인가:
|
|
106
|
+
// Read 세 개를 동시에 하는 것은 안전하다 — 서로 안 건드린다.
|
|
107
|
+
// Write·Edit 을 동시에 돌리면 같은 파일을 두 갈래로 고칠 수 있고,
|
|
108
|
+
// 되돌리기 스냅샷 순서도 뒤엉킨다. Bash 는 무슨 짓을 할지 알 수 없다.
|
|
109
|
+
// 그래서 '읽기만 하는 것' 이라고 확실한 도구만 묶는다.
|
|
110
|
+
const 읽기전용 = new Set(['Read', 'Glob', 'Grep', 'Skill', 'WebFetch']);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 호출 목록을 '같이 돌려도 되는 덩어리' 로 자른다.
|
|
114
|
+
* 읽기 전용이 이어지면 한 덩어리, 그 밖의 것은 하나씩 따로.
|
|
115
|
+
*/
|
|
116
|
+
export function 묶기(calls) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const call of calls) {
|
|
119
|
+
const 안전 = 읽기전용.has(call.name);
|
|
120
|
+
const 끝 = out.at(-1);
|
|
121
|
+
if (안전 && 끝?.parallel) 끝.calls.push(call);
|
|
122
|
+
else out.push({ parallel: 안전, calls: [call] });
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function* run(session, ctx, userText, { signal = null, 깊이 = 0, 그림들 = null, 끼어들기 = null } = {}) {
|
|
128
|
+
/*
|
|
129
|
+
* 되돌리기 턴은 **부모만** 연다.
|
|
130
|
+
*
|
|
131
|
+
* 하위 작업이 제 턴을 열면 `/undo` 한 번이 하위가 만든 것만 되돌리고
|
|
132
|
+
* 부모가 만든 것은 남긴다 — 반쪽만 되돌아간 폴더가 된다. 사람 눈에는
|
|
133
|
+
* 한 번 시킨 일이니 한 번에 되돌아가야 맞다.
|
|
134
|
+
*
|
|
135
|
+
* 턴을 **사람 말을 넣기 전에** 연다. 그래야 그 말 자체가 이 턴의 첫 말로
|
|
136
|
+
* 표시되고, 되감을 때 시킨 말까지 같이 걷힌다. 시킨 말만 남으면 모델은
|
|
137
|
+
* 되돌린 일을 또 하려 든다.
|
|
138
|
+
*/
|
|
139
|
+
if (!깊이) session.턴시작(ctx.history.nextTurn());
|
|
140
|
+
// @ 로 그림을 지목했으면 그 말과 함께 실어 보낸다 (backend/vision.js).
|
|
141
|
+
session.push(그림들?.length
|
|
142
|
+
? 그림메시지(session.conn?.kind, { 글: userText, 그림들 })
|
|
143
|
+
: { role: 'user', content: userText });
|
|
144
|
+
ctx.audit.turn(깊이 ? `[하위작업 ${깊이}겹] ${userText}` : userText);
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* 중단 신호를 도구도 볼 수 있게 여기 걸어 둔다.
|
|
148
|
+
*
|
|
149
|
+
* 없으면 Ctrl+C 가 '도는 도구' 를 못 멈춘다 — 루프는 끊겼는데 Bash 가 띄운
|
|
150
|
+
* 프로세스는 계속 돌고, 화면은 `▶ Bash(npm run dev)` 에서 멈춘 채다.
|
|
151
|
+
* 터미널을 닫는 수밖에 없었다.
|
|
152
|
+
*
|
|
153
|
+
* ctx 를 복사해서 넘기지 않고 그대로 두는 데는 이유가 있다. Read 가 알아낸
|
|
154
|
+
* 인코딩을 ctx.enc 에 적어 두고 Edit 이 그걸 본다. 복사본에 적으면 그 기억이
|
|
155
|
+
* 사라져서, 사내 CP949 문서가 한 번 고치는 것만으로 UTF-8 이 되어 버린다.
|
|
156
|
+
*/
|
|
157
|
+
ctx.signal = signal;
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* 이번에 사람이 실제로 친 말. 도구가 볼 수 있게 여기 걸어 둔다.
|
|
161
|
+
*
|
|
162
|
+
* Ask 관문(agent/askcheck.js)이 이걸 본다 — "사용자가 이미 적어 준 것을
|
|
163
|
+
* 되묻고 있나" 는 원문 없이는 판단할 수 없다. 프롬프트에는 들어 있지만
|
|
164
|
+
* 그건 모델이 보는 것이고, 우리 코드가 볼 수 있는 자리가 없었다.
|
|
165
|
+
*
|
|
166
|
+
* 하위 작업에서는 그 하위가 받은 할일이 들어온다. 그게 맞다 — 하위에게
|
|
167
|
+
* '사용자의 말' 은 자기가 받은 할일이다.
|
|
168
|
+
*/
|
|
169
|
+
ctx.요청 = String(userText ?? '');
|
|
170
|
+
// 이번 턴에 물어본 것. 관문이 "같은 것을 또 묻나" 를 보는 자리다 (agent/askcheck.js).
|
|
171
|
+
ctx.물은것 = [];
|
|
172
|
+
// 접힐 때 요약에 뭉개지지 않게 원문을 세션에도 박아 둔다 (compact.js 의 못박은요청).
|
|
173
|
+
session.이번요청 = ctx.요청;
|
|
174
|
+
|
|
175
|
+
const conn = session.conn;
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
* ── 바깥 모델에게는 파일 속 비밀도 가린다 ───────────────────────────
|
|
179
|
+
*
|
|
180
|
+
* safety/secrets.js 는 「파일 내용은 안 가린다」 로 정해 두었다. 가리면
|
|
181
|
+
* 모델이 가려진 글을 보고 그대로 되돌려 써서 **사람의 열쇠가 지워지기**
|
|
182
|
+
* 때문이다. 로컬 모델만 쓰던 동안에는 그 손해가 이득보다 컸다 — 어차피
|
|
183
|
+
* 이 컴퓨터 밖으로 안 나가니까.
|
|
184
|
+
*
|
|
185
|
+
* 바깥으로 나가는 순간 저울이 뒤집힌다. Read 한 번이면 `.env` 가 통째로
|
|
186
|
+
* 남의 서버 로그에 남는다. 그건 되돌릴 수가 없다.
|
|
187
|
+
*
|
|
188
|
+
* 되돌려 쓰는 사고는 이제 다른 자리에서 막는다 — Write·Append·Edit 이
|
|
189
|
+
* 가린 표를 파일에 못 쓴다(tools/index.js 의 가린표되돌리나). 그 자물쇠가
|
|
190
|
+
* 생겼기 때문에 여기서 가릴 수 있게 된 것이다. 순서가 중요하다.
|
|
191
|
+
*/
|
|
192
|
+
const 바깥으로나감 = 바깥인가(conn.base);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 같은 실패가 되풀이되는지 센다.
|
|
196
|
+
*
|
|
197
|
+
* 모델은 같은 것을 다시 해도 다른 결과가 나올 거라고 믿는 쪽으로 잘 기운다.
|
|
198
|
+
* 특히 오류 문구가 원인을 안 짚어 줄 때 그렇다. 그러면 걸음 수 상한에 닿을
|
|
199
|
+
* 때까지 같은 호출을 반복하는데, 그동안 컨텍스트는 계속 차오르고 사람은
|
|
200
|
+
* 화면만 보며 기다린다. 71초 동안 열세 번을 부르고 아무것도 안 만든 적이 있다.
|
|
201
|
+
*
|
|
202
|
+
* 걸음 수 상한(maxSteps)으로는 못 막는다 — 그건 '잘 되고 있는 긴 작업' 과
|
|
203
|
+
* '헛도는 작업' 을 구분하지 못한다. 여기서는 같은 자리를 반복하는 것만 본다.
|
|
204
|
+
*/
|
|
205
|
+
const 막힘 = new Map();
|
|
206
|
+
/*
|
|
207
|
+
* 몇 번까지 봐 줄까는 **이 모델을 겪어 본 만큼** 정한다 (agent/card.js).
|
|
208
|
+
*
|
|
209
|
+
* 되풀이가 버릇인 모델에서는 세 번까지 기다릴 이유가 없다. 되풀이한 만큼
|
|
210
|
+
* 컨텍스트가 차고 그만큼 요약이 빨리 온다 — 걸음만 태우는 것이 아니다.
|
|
211
|
+
* 겪은 것이 모자라면 여태 쓰던 3 그대로다.
|
|
212
|
+
*/
|
|
213
|
+
const 이카드 = ctx?.카드 ?? null;
|
|
214
|
+
const MAX_SAME = 이카드?.조정?.같은것한계 ?? 3;
|
|
215
|
+
let 멈출까 = null;
|
|
216
|
+
const 막힘셈 = (call, 이유) => {
|
|
217
|
+
// 파일 하나가 아니라 **그 파일** 을 센다. 이름만 세면 서로 다른 파일 세 개를
|
|
218
|
+
// 고치다 실패한 것이 한 덩어리로 뭉쳐 턴이 죽는다. 다섯 군데 중 두 군데만
|
|
219
|
+
// 고쳐 놓고 '헛돌고 있어 멈췄습니다' 가 되는 것이 그 모습이다.
|
|
220
|
+
const 어디 = call.args?.file_path ?? call.args?.path ?? call.args?.pattern ?? call.args?.command
|
|
221
|
+
?? call.args?.purpose ?? call.args?.목적 ?? '';
|
|
222
|
+
const 서명 = `${call.name}|${이유}|${String(어디).slice(0, 200)}`;
|
|
223
|
+
const n = (막힘.get(서명) ?? 0) + 1;
|
|
224
|
+
막힘.set(서명, n);
|
|
225
|
+
return n >= MAX_SAME;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 일이 나아갔다고 표시한다.
|
|
230
|
+
*
|
|
231
|
+
* 잘린 것을 살려 파일이 자라는 중이면 헛도는 게 아니다. 그런데 겉모습은 똑같다 —
|
|
232
|
+
* 같은 도구를, 같은 파일에, 여러 번 부른다. 그 표시만 보고 막으면 세 번째
|
|
233
|
+
* 이어붙이기에서 턴이 죽고, 파일은 또 반쪽으로 남는다.
|
|
234
|
+
* 그래서 '나아가고 있다' 는 사실이 확인되면 셈을 되돌린다.
|
|
235
|
+
*/
|
|
236
|
+
const 나아감 = () => 막힘.clear();
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 이번 턴에 손댄 파일들. 턴이 끝날 때 **디스크를 보고** 사실을 말해 준다.
|
|
240
|
+
*
|
|
241
|
+
* 모델은 도구가 실패해도 "만들었습니다" 로 답을 맺는 일이 있다. 화면에는
|
|
242
|
+
* 그 말만 남고, 사용자는 믿고 다음 일로 넘어간다 — 파일은 없는데.
|
|
243
|
+
* 말이 아니라 파일을 보고 확인해야 그 틈이 안 생긴다.
|
|
244
|
+
*/
|
|
245
|
+
const 손댄파일 = new Set();
|
|
246
|
+
const 마무리 = () => [...손댄파일].map(파일현황);
|
|
247
|
+
|
|
248
|
+
// 살려 쓴 파일이 지난번보다 실제로 커졌는지 보려고 크기를 기억해 둔다.
|
|
249
|
+
const 살린크기 = new Map();
|
|
250
|
+
|
|
251
|
+
// 이번 턴에 서버에게서 한계를 배웠나. 배웠는데도 또 거절당하면 다른 문제다 —
|
|
252
|
+
// 그때는 끝없이 다시 부르지 않고 오류를 그대로 보여 준다.
|
|
253
|
+
let 배운적 = false;
|
|
254
|
+
/*
|
|
255
|
+
* 이번 턴에 **전선 모양**을 배운 적이 있나 (backend/wire.js).
|
|
256
|
+
*
|
|
257
|
+
* 한계(숫자)와는 다른 축이다. 이쪽은 「무슨 칸을 보내면 안 되나」 다 —
|
|
258
|
+
* Opus 5 에 budget_tokens 를 보내면 400 이고, 그 400 은 화면에서 열쇠가
|
|
259
|
+
* 틀린 것과 구별이 안 된다. 한 번 배우면 카드에 적고 다시 안 밟는다.
|
|
260
|
+
* 한 턴에 한 번뿐이다 — 고치고도 또 거절당하면 다른 탈이다.
|
|
261
|
+
*/
|
|
262
|
+
let 전선배운적 = false;
|
|
263
|
+
// 이번 턴에 자리가 없어 기억을 비운 적이 있나. 한 턴에 한 번뿐이다 —
|
|
264
|
+
// 비우고도 또 막히면 자리 문제가 아니라 다른 탈이다.
|
|
265
|
+
let 비운적 = false;
|
|
266
|
+
const 짧게말 = (s) => String(s ?? '').replace(/\s+/g, ' ').slice(0, 200);
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 이미 부른 것을 똑같이 또 부르는지 본다.
|
|
270
|
+
*
|
|
271
|
+
* 실패만 세면 이 자리를 못 잡는다. 실제 화면에 이렇게 찍혔다 —
|
|
272
|
+
* ☰ TodoWrite(3건) 같은 목록, 다섯 번
|
|
273
|
+
* ❋ Glob(**\/*) 4개 같은 패턴, 네 번
|
|
274
|
+
* 전부 '성공' 이다. 그런데 아무것도 안 나아가고, 같은 결과가 매번 컨텍스트에
|
|
275
|
+
* 한 벌씩 더 쌓인다. 파일 목록 하나가 네 번 들어가면 그만큼 밀려난다.
|
|
276
|
+
*
|
|
277
|
+
* 그래서 두 번째부터는 결과를 다시 안 싣는다 — 짧게 '앞에서 부른 것과 같다'
|
|
278
|
+
* 고만 알려 준다. 짝은 맞아야 하니 답 자체는 반드시 보낸다.
|
|
279
|
+
*
|
|
280
|
+
* 결과가 달라졌으면 그대로 싣는다. 고치고 나서 다시 읽는 것은 정상이다.
|
|
281
|
+
*/
|
|
282
|
+
const 부른것 = new Map();
|
|
283
|
+
/*
|
|
284
|
+
* 그 부름을 마지막으로 봤을 때, 이 턴이 파일을 몇 개나 손댔었나.
|
|
285
|
+
*
|
|
286
|
+
* 되풀이를 셀 때 이 값과 견준다. 그 사이에 파일이 늘었으면 일이 되고
|
|
287
|
+
* 있는 것이라 되풀이 셈을 처음부터 다시 센다 — 까닭은 세는 자리에.
|
|
288
|
+
*/
|
|
289
|
+
const 반복진척 = new Map();
|
|
290
|
+
/*
|
|
291
|
+
* 서명은 한 모양으로 맞춰 놓고 견준다 (NFC).
|
|
292
|
+
*
|
|
293
|
+
* 맥은 파일 이름을 자모를 쪼개서 돌려준다. 그래서 `Read(PDF_경로.md)` 와
|
|
294
|
+
* `Read(PDF_경로.md)` 가 글자로는 다른 호출이 된다 — 같은 파일을 두 번 읽고
|
|
295
|
+
* 두 번 싣고도 되풀이로 안 잡힌다. 실제로 3,250줄짜리 파일이 그렇게 두 번 들어갔다.
|
|
296
|
+
*/
|
|
297
|
+
const 서명만들기 = (call) => {
|
|
298
|
+
try { return `${call.name}(${JSON.stringify(call.args ?? {})})`.normalize('NFC'); }
|
|
299
|
+
catch { return `${call.name}(?)`; }
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 중단하고 나갈 때 대화를 성한 상태로 남긴다.
|
|
304
|
+
*
|
|
305
|
+
* 모델이 도구를 부르겠다고 해 놓고 결과가 안 들어간 채로 끝나면, 다음에 이어할 때
|
|
306
|
+
* 그 배열을 그대로 보낼 수 없다 — 서버가 400 을 낸다. 그래서 부른 만큼
|
|
307
|
+
* '중단됨' 결과를 채워 짝을 맞춘다.
|
|
308
|
+
*/
|
|
309
|
+
const 짝맞추기 = (왜 = '사용자가 중단했습니다. 실행하지 않았습니다.') => {
|
|
310
|
+
const last = session.messages.at(-1);
|
|
311
|
+
if (last?.role !== 'assistant') return;
|
|
312
|
+
// 부른 자리는 규격마다 다르다 — 읽는 규칙은 backend/adapter.js 의 부른것들 하나뿐이다.
|
|
313
|
+
const 부른것 = 부른것들(last);
|
|
314
|
+
if (!부른것.length) return;
|
|
315
|
+
// 결과가 어느 role 에 실리는지도 규격마다 다르다. `role === 'tool'` 로
|
|
316
|
+
// 세면 Anthropic 꼴에서 언제나 0 이라, 지어낸 번호가 전부 겹친다.
|
|
317
|
+
const 이미 = session.messages.filter(도구결과인가).length;
|
|
318
|
+
for (const t of 부른것) {
|
|
319
|
+
session.push(toolMessage(conn.kind, {
|
|
320
|
+
callId: t.id ?? `call_${이미 + 1}`,
|
|
321
|
+
name: t.name ?? '?',
|
|
322
|
+
content: 왜,
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/*
|
|
327
|
+
* 이 턴에 모델에게 보여 줄 도구.
|
|
328
|
+
*
|
|
329
|
+
* 하위 작업이면 `도구제한` 이 차 있다 — **부모가 가졌던 것** 이다. 그것을
|
|
330
|
+
* 그대로 넘기면 toolSchemas 가 거기에 다시 하위의 작업 모드를 걸러 얹으므로,
|
|
331
|
+
* 하위가 가질 수 있는 것은 언제나 부모가 가졌던 것의 부분집합이 된다.
|
|
332
|
+
*
|
|
333
|
+
* 이게 없으면 구멍이 하나 생긴다. 설계 모드는 "파일을 안 바꾼다" 는 약속인데,
|
|
334
|
+
* 하위가 제 모드를 code 로 골라 버리면 그 약속이 하위에서 깨진다. 화면에는
|
|
335
|
+
* 여전히 설계 모드라고 떠 있는 채로 파일이 바뀐다. task.js 의 하위모드() 가
|
|
336
|
+
* 모드 쪽에서 한 겹 막고, 여기가 도구 쪽에서 한 겹 더 막는다.
|
|
337
|
+
*/
|
|
338
|
+
const tools = toolSchemas(session.도구제한 ?? null, {
|
|
339
|
+
hasSkills: (session.skills?.length ?? 0) > 0,
|
|
340
|
+
web: session.web !== false && !isOffline(), // 오프라인이면 웹 도구는 아예 안 보여 준다
|
|
341
|
+
work: session.effectiveWork(), // 작업 모드가 쓰는 것만 (modes.js)
|
|
342
|
+
// 밖에서 붙인 도구(MCP). 붙은 것이 없으면 아무것도 안 는다.
|
|
343
|
+
mcp: ctx.mcp ?? null,
|
|
344
|
+
// 이 자리에 언어 서버가 있을 때만 Def·Refs 를 보여 준다 (tools/lsp.js).
|
|
345
|
+
// 없는 자리에서 목록에 세워 두면 모델이 부르고, 실패를 받고, 또 부른다.
|
|
346
|
+
lsp: session.lsp === true,
|
|
347
|
+
// 창이 좁으면 도구 설명을 줄여 싣는다 (budget.js 의 설명길이).
|
|
348
|
+
// 도구를 빼는 게 아니라 설명만 줄이므로 할 수 있는 일은 안 달라진다.
|
|
349
|
+
ctx: conn.ctx ?? null,
|
|
350
|
+
// 그림을 볼 수 있는 모델일 때만 Read 설명에 그림 이야기를 넣는다.
|
|
351
|
+
// 못 보는데 넣어 두면 모델이 화면 사진을 열려 들고, 그때마다 한 걸음이 헛간다.
|
|
352
|
+
vision: conn.vision === true,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
/*
|
|
356
|
+
* 하위 작업에게 물려줄 도구 이름들.
|
|
357
|
+
*
|
|
358
|
+
* MCP 도구는 뺀다. toolSchemas 는 이름을 받으면 TOOLS 표에서 찾는데, MCP
|
|
359
|
+
* 이름은 그 표에 없어서 undefined 를 읽다 죽는다. MCP 는 이름 목록이 아니라
|
|
360
|
+
* ctx.mcp 로 따로 넘어가므로 하위도 그 길로 똑같이 받는다.
|
|
361
|
+
*/
|
|
362
|
+
const 내도구 = tools.map((t) => t.function.name).filter((n) => !n.startsWith('mcp__'));
|
|
363
|
+
/*
|
|
364
|
+
* 깊이 상한. 부모(0) → 하위(1) → 하위의 하위(2) 까지다.
|
|
365
|
+
*
|
|
366
|
+
* 상한에 닿으면 목록에서 Task 를 뺀다. "더 쪼개지 마라" 고 부탁하지 않는다 —
|
|
367
|
+
* 모델은 부탁을 잊고, 잊으면 하위가 하위를 끝없이 낳는다.
|
|
368
|
+
*/
|
|
369
|
+
const 자식도구 = 깊이 + 1 >= 최대깊이 ? 내도구.filter((n) => n !== 'Task') : 내도구;
|
|
370
|
+
// 모드마다 생각의 배분과 걸음 수가 다르다. 사용자가 따로 정했으면 그걸 존중한다.
|
|
371
|
+
const 모드 = workMode(session.effectiveWork());
|
|
372
|
+
const effort = session.effortSet ? session.effort : (모드.effort ?? session.effort);
|
|
373
|
+
/*
|
|
374
|
+
* ── 강도는 **천장**이고, 이번 턴 값은 시킨 말이 정한다 (agent/effort.js) ──
|
|
375
|
+
*
|
|
376
|
+
* `/think max` 는 「언제나 max」 가 아니라 「max 까지 써도 된다」 는 뜻이다.
|
|
377
|
+
* 「안녕」 한 마디에 max 로 생각하면 느려지고 값만 나간다 — 생각 토큰은
|
|
378
|
+
* 출력 단가로 나가서, 그 한 마디가 그 세션에서 제일 비싼 토큰이 된다.
|
|
379
|
+
*
|
|
380
|
+
* 대화가 쌓인 뒤에는 안 움직인다. 강도가 바뀌면 캐시가 깨지는데, 그때는
|
|
381
|
+
* 앞머리를 다시 보내는 값이 아끼는 값보다 크다.
|
|
382
|
+
*
|
|
383
|
+
* 한 번 정하면 이 턴 안에서는 안 바꾼다. while 바깥에서 정하는 것이 그
|
|
384
|
+
* 뜻이다 — 걸음마다 흔들리면 그것이 곧 캐시가 매번 새로 엮인다는 말이다.
|
|
385
|
+
*/
|
|
386
|
+
const 천장 = session.thinkSet ? session.think : (모드.think ?? session.think);
|
|
387
|
+
const think = 자동강도(userText, 천장, {
|
|
388
|
+
대화크기: session.messages.length,
|
|
389
|
+
켜짐: session.autoThink !== false,
|
|
390
|
+
});
|
|
391
|
+
/*
|
|
392
|
+
* 바깥 모델에서는 단계별로도 안 움직인다.
|
|
393
|
+
*
|
|
394
|
+
* 로컬은 자기 KV 캐시를 쓰고 토큰 값을 따로 안 내므로 여태처럼 첫 판단만
|
|
395
|
+
* 세게, 이어가기는 얕게 간다. 바깥은 그 배분 하나가 걸음마다 요청을
|
|
396
|
+
* 바꿔 놓아 캐시를 깬다.
|
|
397
|
+
*/
|
|
398
|
+
const 강도고정 = 바깥으로나감;
|
|
399
|
+
/*
|
|
400
|
+
* 걸음 수 상한은 **모델에 맞춰** 정한다 (budget.js).
|
|
401
|
+
*
|
|
402
|
+
* 전에는 모드마다 숫자가 박혀 있었다(코드 24회). 그 값이 맞는 모델은 하나도
|
|
403
|
+
* 없다 — 8k 모델에는 너무 크고, 655k 모델은 여유가 96% 남았는데도 만들다
|
|
404
|
+
* 만 채로 끊겼다. 사람이 직접 준 값이 있으면 그것이 먼저다.
|
|
405
|
+
*/
|
|
406
|
+
const maxSteps = session.stepsSet ? session.maxSteps : 걸음수(모드.id, conn.ctx);
|
|
407
|
+
const attempted = new Set(); // 같은 변경성 명령을 두 번 실행하지 않기 위한 기록
|
|
408
|
+
let steps = 0;
|
|
409
|
+
let lastToolFailed = false; // 직전 단계에서 도구가 오류를 냈나 → 다음 판단은 세게
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* ── 조사만 하고 "무엇을 도와드릴까요" 로 끝내는 것 ────────────
|
|
413
|
+
*
|
|
414
|
+
* 실제로 이렇게 끝난 턴이다. 할 일 셋을 적어 놓고, 파일 스물일곱 개를
|
|
415
|
+
* 읽고, 그중 하나도 안 끝낸 채로 —
|
|
416
|
+
*
|
|
417
|
+
* ☰ TodoWrite(3건) ▶ 문서와 실행 구조를 확인한다 ☐ README 를 정리한다 …
|
|
418
|
+
* ◧ Read × 스물일곱
|
|
419
|
+
* ▌ 무엇을 도와드릴까요? 프로젝트 구조와 실행 방식은 확인했습니다.
|
|
420
|
+
* ── 30.9초 · 도구 27회
|
|
421
|
+
*
|
|
422
|
+
* 사람은 "문서 정리해줘" 라고 말했고 30초를 기다렸는데, 바뀐 파일은 없다.
|
|
423
|
+
* 다시 "정리해줘" 라고 하면 또 처음부터 읽는다 — 조사한 것은 매번 버려진다.
|
|
424
|
+
* 걸음 수 상한으로도, 반복 감지로도 이 자리는 안 잡힌다. 모델은 오류 없이,
|
|
425
|
+
* 정중하게, 제 일을 사람에게 되돌려주고 끝낸 것이다.
|
|
426
|
+
*
|
|
427
|
+
* 그래서 한 번만 되민다. 세 가지가 다 맞을 때만이다 —
|
|
428
|
+
* · 이번 턴에 바꾼 파일이 하나도 없다
|
|
429
|
+
* · 파일을 바꿀 수 있는 모드다 (설계·묻기 모드는 안 바꾸는 것이 정상이다)
|
|
430
|
+
* · 되묻는 말로 끝났거나, 남은 할 일이 있는데 답이 한 줄이다
|
|
431
|
+
*
|
|
432
|
+
* 한 번뿐인 이유: 두 번 밀면 못 하는 일을 두고 실랑이가 된다. 한 번 밀었는데도
|
|
433
|
+
* 또 물으면 그건 진짜 막힌 것이니, 그 말을 사람에게 그대로 보여 준다.
|
|
434
|
+
*
|
|
435
|
+
* 물어볼 것이 진짜로 있을 때는 Ask 도구가 있다. 그것은 도구 호출이라
|
|
436
|
+
* 여기까지 오지 않는다 — 막히는 것은 글로 되묻고 턴을 닫는 쪽뿐이다.
|
|
437
|
+
*/
|
|
438
|
+
let 민적 = false;
|
|
439
|
+
/*
|
|
440
|
+
* 인사에는 안 민다.
|
|
441
|
+
*
|
|
442
|
+
* 「안녕」 한 마디에 모델이 "안녕하세요! 무엇을 도와드릴까요?" 라고 답하면
|
|
443
|
+
* 그건 **맞는 답**이다. 그런데 그 말이 되묻는말에 걸려서 되밀렸다 —
|
|
444
|
+
*
|
|
445
|
+
* ❯ 안녕
|
|
446
|
+
* ▌ 안녕하세요! 무엇을 도와드릴까요?
|
|
447
|
+
* ↺ 읽기만 하고 끝내려고 해서 한 번 되밀었습니다
|
|
448
|
+
* ▌ 안녕하세요! 반갑습니다.
|
|
449
|
+
* ── 6.8초
|
|
450
|
+
*
|
|
451
|
+
* 인사 한 마디에 모델을 두 번 부른다. 로컬 모델에서 그 왕복이 6.8초다.
|
|
452
|
+
* 되밀기는 「시킨 일을 안 하고 되돌려준」 자리를 잡으라고 만든 것인데,
|
|
453
|
+
* 인사는 시킨 일이 없다. 시킨 것이 없으면 안 한 것도 없다.
|
|
454
|
+
*/
|
|
455
|
+
/*
|
|
456
|
+
* 무엇이 인사인지는 agent/effort.js 가 한 벌만 들고 있다.
|
|
457
|
+
*
|
|
458
|
+
* 여기와 거기가 각자 규칙을 들고 있으면 언젠가 갈라진다 — 한쪽은 인사로
|
|
459
|
+
* 보고 강도를 낮추는데 다른 쪽은 인사가 아니라고 되미는, 설명하기 어려운
|
|
460
|
+
* 화면이 된다.
|
|
461
|
+
*/
|
|
462
|
+
const 인사인가 = 인사말인가(userText);
|
|
463
|
+
const 되묻는말 = /무엇을\s*도와|무엇을\s*해\s*드릴|원하시는\s*(작업|것)|어떤\s*(작업|것)\s*(을|부터)|말씀해\s*주세요|알려\s*주세요|지시해\s*주세요|어떻게\s*할까요|해\s*드릴까요|진행할까요|what would you like|how can i (help|assist)|let me know (what|which|how)|shall i\b|would you like me to/i;
|
|
464
|
+
|
|
465
|
+
/** 이번 답을 되밀까. 밀 이유를 돌려주고, 아니면 null. */
|
|
466
|
+
const 밀어줄까 = (글) => {
|
|
467
|
+
if (민적 || 깊이) return null;
|
|
468
|
+
if (인사인가) return null; // 시킨 것이 없으면 안 한 것도 없다
|
|
469
|
+
if (손댄파일.size) return null; // 뭐라도 바꿨으면 일은 한 것이다
|
|
470
|
+
if (!모드.tools.includes('Write')) return null; // 안 바꾸는 모드는 그게 맞다
|
|
471
|
+
const 말 = String(글 ?? '').trim();
|
|
472
|
+
if (되묻는말.test(말)) return '되물음';
|
|
473
|
+
const 남은 = (ctx.todos ?? []).filter((x) => x.state !== 'done');
|
|
474
|
+
// 진짜 보고는 길다. 한 줄로 끝났고 할 일이 남았으면 놓은 것이다.
|
|
475
|
+
if (남은.length && 말.length < 120) return '할일남음';
|
|
476
|
+
return null;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/*
|
|
480
|
+
* ── 시킨 것을 다 했나 (agent/asks.js) ────────────────────────────────
|
|
481
|
+
*
|
|
482
|
+
* 위의 되밀기는 **아무것도 안 바꿨을 때**만 본다(손댄파일.size 로 빠져나간다).
|
|
483
|
+
* 그런데 제보받은 것은 그게 아니었다 — 네 가지를 시키면 셋을 고치고 끝냈다.
|
|
484
|
+
* 뭐라도 고쳤으니 되밀기는 안 걸리고, 안 한 하나는 말없이 사라진다.
|
|
485
|
+
*
|
|
486
|
+
* 그래서 여기서 따로 대조한다. 시킨 것을 항목으로 쪼개 두고, 항목마다
|
|
487
|
+
* 자국이 남았는지 본다 — 할 일 목록에 있나, 손댄 파일 이름에 있나,
|
|
488
|
+
* 마지막 보고에 있나. 하나도 없으면 잊은 것이다.
|
|
489
|
+
*
|
|
490
|
+
* 한 번만 민다. 밀고도 그대로면 사람에게 말한다 — 두 번 세 번 미는 것은
|
|
491
|
+
* 모델이 못 하는 일을 억지로 시키는 꼴이고, 그 왕복은 사람이 낸다.
|
|
492
|
+
*/
|
|
493
|
+
let 빠뜨림민적 = false;
|
|
494
|
+
const 빠뜨린것 = (글) => {
|
|
495
|
+
// 하위 작업은 사람이 시킨 말이 아니다. 거기 할일은 부모가 이미 쪼개 준 것이다.
|
|
496
|
+
if (깊이) return [];
|
|
497
|
+
return 빠진것({
|
|
498
|
+
요청: userText,
|
|
499
|
+
자국: [
|
|
500
|
+
...(ctx.todos ?? []).map((x) => x.text),
|
|
501
|
+
...손댄파일,
|
|
502
|
+
String(글 ?? ''),
|
|
503
|
+
],
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/*
|
|
508
|
+
* 걸음이 끊긴 자리에서 대조할 '마지막으로 한 말'.
|
|
509
|
+
*
|
|
510
|
+
* done 은 방금 받은 답(msg.content)을 자국으로 넘긴다. limit·stuck 에는
|
|
511
|
+
* 그 답이 없다 — 도구를 부르다 끊겼기 때문이다. 그래도 모델이 앞서 글로
|
|
512
|
+
* 적어 둔 것은 자국이 되므로 대화에서 마지막 assistant 글을 꺼내 쓴다.
|
|
513
|
+
*/
|
|
514
|
+
const 마지막말 = () => String(
|
|
515
|
+
[...session.messages].reverse().find((m) => m.role === 'assistant' && m.content)?.content ?? '',
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const 되미는말 = () => {
|
|
519
|
+
const 시킨 = String(userText ?? '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
520
|
+
return 지시말() === 'en'
|
|
521
|
+
? `Nothing has been changed yet. What was asked: "${시킨}"\n`
|
|
522
|
+
+ 'The reading is done. Do it now — do not ask back. Deciding how is your job.\n'
|
|
523
|
+
+ 'If a tool is genuinely blocking you, say in one line what blocked you.'
|
|
524
|
+
: `아직 아무것도 안 바꿨습니다. 시킨 것은 이것입니다: "${시킨}"\n`
|
|
525
|
+
+ '읽는 것은 끝났습니다. 되묻지 말고 지금 하세요 — 어떻게 할지 정하는 것이 당신 일입니다.\n'
|
|
526
|
+
+ '정말 도구가 막고 있으면 무엇이 막았는지 한 줄로 말하세요.';
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/*
|
|
530
|
+
* ── 자리를 만든다 (접기 → 요약) ────────────────────────────────────────
|
|
531
|
+
*
|
|
532
|
+
* 여태 이 검사는 **도구를 한 번 돌린 뒤에만** 했다. 그래서 첫 부름 전에는
|
|
533
|
+
* 아무리 꽉 차 있어도 그대로 보냈다. 실제로 이렇게 죽는다 —
|
|
534
|
+
*
|
|
535
|
+
* 컨텍스트가 78% 인 대화에 긴 파일을 하나 붙인다
|
|
536
|
+
* → 첫 요청이 창을 넘겨 서버가 거절한다
|
|
537
|
+
* → 배운 값으로 다시 부른다(같은 이력 그대로) → 또 거절
|
|
538
|
+
* → 이번엔 배운 적이 있으니 오류로 끝난다
|
|
539
|
+
*
|
|
540
|
+
* 사람이 보는 것은 「턴이 죽었다」 이고, /clear 하고 시킨 말을 다시 쳐야
|
|
541
|
+
* 한다. 붙인 파일도 다시 붙여야 한다. 검사는 걸음마다 **부르기 전에**
|
|
542
|
+
* 해야 맞다 — 자리가 모자란 것은 부른 뒤가 아니라 부르기 전에 안다.
|
|
543
|
+
*
|
|
544
|
+
* 문턱은 안 건드린다(FOLD_AT 0.55 · COMPACT_AT 0.8). 여기서 바뀐 것은
|
|
545
|
+
* '언제 재느냐' 하나다.
|
|
546
|
+
*
|
|
547
|
+
* @returns {boolean} 멈추라는 것이 닿아서 턴을 접어야 하나
|
|
548
|
+
*/
|
|
549
|
+
let 접다멈췄나 = false;
|
|
550
|
+
const 자리만들기 = async function* () {
|
|
551
|
+
접다멈췄나 = false;
|
|
552
|
+
/*
|
|
553
|
+
* 요약해서 접기 **전에** 도구 결과부터 접는다.
|
|
554
|
+
*
|
|
555
|
+
* 자리를 먹는 것은 대개 사람 말이 아니라 옛날에 읽어 둔 파일이다. 그쪽을
|
|
556
|
+
* 먼저 비우면 요약 압축을 한참 미룰 수 있고, 미루는 동안 대화는 한 글자도
|
|
557
|
+
* 안 잃는다. 그래도 차면 아래에서 통째로 요약한다.
|
|
558
|
+
*/
|
|
559
|
+
if (shouldFold(session)) {
|
|
560
|
+
const f = foldToolResults(session);
|
|
561
|
+
if (f.접은것) {
|
|
562
|
+
/*
|
|
563
|
+
* 접힌 파일은 **기억에서도 지운다** (agent/filemem.js).
|
|
564
|
+
*
|
|
565
|
+
* 내용은 접혀 사라졌는데 「이미 읽었다」 는 표만 남으면, 그 파일을
|
|
566
|
+
* 다시 읽을 때 「앞에서 읽은 그대로입니다」 가 돌아간다 — 모델은
|
|
567
|
+
* 대화 어디에도 없는 글을 가리키는 쪽지를 받고, 결국 또 읽는다.
|
|
568
|
+
* 같은 파일을 두 번 읽는 자리가 여기였다.
|
|
569
|
+
*/
|
|
570
|
+
for (const x of f.접은것들 ?? []) {
|
|
571
|
+
if (!x.경로) continue;
|
|
572
|
+
// 열쇠를 맞춰서 지운다 — 까닭은 agent/compact.js 의 접힌파일열쇠.
|
|
573
|
+
const 열쇠 = 접힌파일열쇠(x.경로, ctx.scope);
|
|
574
|
+
session.파일기억?.잊기(열쇠);
|
|
575
|
+
if (열쇠 !== x.경로) session.파일기억?.잊기(x.경로);
|
|
576
|
+
}
|
|
577
|
+
yield { type: 'folded', ...f };
|
|
578
|
+
}
|
|
579
|
+
// 그림은 사람 말 자리에 실려서 위 접기가 못 건드린다. 따로 뺀다.
|
|
580
|
+
const g = foldImages(session);
|
|
581
|
+
if (g.뺀것) yield { type: 'images_folded', ...g };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 컨텍스트가 차오르면 오래된 대화를 '요약해서' 접는다. 그냥 자르면 하던 일을 잊는다.
|
|
585
|
+
if (shouldCompact(session)) {
|
|
586
|
+
yield { type: 'compacting' };
|
|
587
|
+
// 요약을 부르다 서버가 막으면 그 알림도 화면으로 — '접는 중' 뒤에 60초를 숨기지 않는다.
|
|
588
|
+
const 편지 = 우편함((onBackoff) => compact(session, { auto: true, signal, onBackoff }));
|
|
589
|
+
for await (const 알림 of 편지.소식()) yield 알림;
|
|
590
|
+
const r = await 편지.부름;
|
|
591
|
+
if (r.aborted) { 접다멈췄나 = true; yield { type: 'aborted', steps, kept: true }; return; }
|
|
592
|
+
if (r.ok) yield { type: 'compacted', ...r };
|
|
593
|
+
else yield { type: 'compact_failed', why: r.why };
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
while (steps < maxSteps) {
|
|
598
|
+
steps++;
|
|
599
|
+
|
|
600
|
+
// 부르기 **전에** 자리를 본다. 왜 여기인지는 자리만들기() 머리말에.
|
|
601
|
+
yield* 자리만들기();
|
|
602
|
+
if (접다멈췄나) return;
|
|
603
|
+
|
|
604
|
+
/*
|
|
605
|
+
* ── 도중에 한 말을 여기서 받는다 (끼어들기) ──────────────────────────
|
|
606
|
+
*
|
|
607
|
+
* 일하는 동안 친 말은 여태 **턴이 끝날 때까지** 기다렸다. 로컬 모델은
|
|
608
|
+
* 한 턴이 몇 분씩 가는데, 그 사이에 "아, 그건 말고 저거" 라고 쳐도
|
|
609
|
+
* 모델은 하던 길을 끝까지 간다. 사람이 할 수 있는 것은 Ctrl+C 로
|
|
610
|
+
* 지금까지 한 것을 통째로 버리는 것뿐이었다.
|
|
611
|
+
*
|
|
612
|
+
* 그래서 걸음마다 **모델을 부르기 직전에** 받아 넣는다. 다음 부름에
|
|
613
|
+
* 그 말이 같이 실리니, 하던 일을 버리지 않고 방향만 튼다.
|
|
614
|
+
*
|
|
615
|
+
* 도구가 도는 중간에는 안 넣는다 — 도구 호출과 그 답 사이에 사람 말이
|
|
616
|
+
* 끼면 대화 모양이 깨져서 게이트웨이가 400 을 준다. 걸음의 머리는
|
|
617
|
+
* 언제나 도구 답이 다 붙은 뒤다.
|
|
618
|
+
*/
|
|
619
|
+
/*
|
|
620
|
+
* 첫 걸음에는 안 받는다.
|
|
621
|
+
*
|
|
622
|
+
* 첫 부름 전에 큐에 남아 있는 것은 **턴이 시작되기 전에** 친 말이다.
|
|
623
|
+
* 그건 원래 다음 턴이 될 것이었는데, 여기서 끌어다 넣으면 사람이 따로
|
|
624
|
+
* 시킨 두 가지가 한 턴으로 뭉친다. 끼어들기는 「가던 길을 트는 것」이지
|
|
625
|
+
* 「줄 서 있는 것을 당겨오는 것」이 아니다.
|
|
626
|
+
*/
|
|
627
|
+
const 끼어든말 = (깊이 || steps <= 1) ? null : 끼어들기?.();
|
|
628
|
+
if (끼어든말) {
|
|
629
|
+
session.push({ role: 'user', content: 끼어든말 });
|
|
630
|
+
ctx.audit.turn(`[도중에 끼어든 말] ${끼어든말}`);
|
|
631
|
+
yield { type: 'steer', text: 끼어든말 };
|
|
632
|
+
}
|
|
633
|
+
/*
|
|
634
|
+
* 이 모델이 실제로 어떻게 하고 있는지 센다 (agent/grade.js).
|
|
635
|
+
*
|
|
636
|
+
* 이름에 70B 라고 적혀 있어도 걸음마다 인자가 잘리면 그건 붙들어 줘야
|
|
637
|
+
* 하는 상태다. 이름은 첫 어림일 뿐이고, 여기서 세는 것이 진짜 근거다.
|
|
638
|
+
* 세기만 한다 — 판단은 grade.js 가 하고, 프롬프트는 다음 걸음부터 달라진다.
|
|
639
|
+
*/
|
|
640
|
+
session.본것?.걸음셈();
|
|
641
|
+
ctx.배움?.모델본것(conn.model, '걸음');
|
|
642
|
+
// 단계마다 필요한 생각의 양이 다르다. effort.js 가 그 배분을 갖고 있다.
|
|
643
|
+
const stage = steps === 1 ? 'plan' : lastToolFailed ? 'fix' : 'work';
|
|
644
|
+
const level = effortFor(think, effort, stage, { 고정: 강도고정 });
|
|
645
|
+
// 상한은 모델 컨텍스트와 지금 찬 양에서 계산한다. 고정 숫자가 아니다.
|
|
646
|
+
// 출력 상한을 아는 값이 둘 있다 — 사용자가 정한 것(/out)과 서버에서 알아낸 것.
|
|
647
|
+
// 사람이 정한 것이 먼저다. 둘 다 없으면 null 이고, 그때만 effort.js 의 MAX_CAP 에 선다.
|
|
648
|
+
const room = { ctx: conn.ctx ?? 0, used: session.breakdown().used, max: conn.maxTokens ?? conn.maxOut ?? null };
|
|
649
|
+
/*
|
|
650
|
+
* 이 모델이 인자를 자주 잘라 먹는다면 처음부터 넉넉히 준다 (agent/card.js).
|
|
651
|
+
*
|
|
652
|
+
* 지금은 잘린 **뒤에야** 상한을 올려 다시 부른다. 한 번은 반드시 버리는 셈이다.
|
|
653
|
+
* 잘리는 것이 이 모델의 버릇으로 재어졌다면 그 한 번을 매번 버릴 이유가 없다.
|
|
654
|
+
* 상한은 최댓값이지 정해진 길이가 아니라서, 짧게 답하는 턴에는 값이 안 든다.
|
|
655
|
+
*/
|
|
656
|
+
const cap = 이카드?.조정?.상한먼저올리기
|
|
657
|
+
? Math.max(tokensFor(effort, stage, room), fullCap(room))
|
|
658
|
+
: tokensFor(effort, stage, room);
|
|
659
|
+
// 천장도 같이 알려 준다 — 화면이 「max 라고 정했는데 왜 medium 인가」 에
|
|
660
|
+
// 답할 수 있어야 한다. 안 그러면 자동 조절이 고장으로 보인다.
|
|
661
|
+
yield { type: 'stage', stage, level, cap, step: steps, 천장, 고정: 강도고정 };
|
|
662
|
+
|
|
663
|
+
const ask = (maxTokens, think) => ({
|
|
664
|
+
messages: session.wire(),
|
|
665
|
+
tools,
|
|
666
|
+
think: thinkFor(conn, think),
|
|
667
|
+
maxTokens,
|
|
668
|
+
signal,
|
|
669
|
+
/*
|
|
670
|
+
* 열쇠받기(safety/authcmd.js)가 걸린 연결에서 쓴다.
|
|
671
|
+
*
|
|
672
|
+
* 두 갈래를 위층으로 올린다 — 「부를까요」 를 사람에게 묻는 길과,
|
|
673
|
+
* 「지금 사내 로그인을 기다리는 중」 을 화면에 적는 길. 여기서 화면을
|
|
674
|
+
* 직접 만지지 않는 것은, 이 루프가 대화 화면 말고 oneshot·ACP 에서도
|
|
675
|
+
* 돌기 때문이다. 그쪽에는 물어볼 사람이 없다.
|
|
676
|
+
*/
|
|
677
|
+
열쇠물어보기: session.열쇠물어보기 ?? null,
|
|
678
|
+
onAuth: (것) => { session.onAuth?.(것); },
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
let msg;
|
|
682
|
+
// 흘러온 글을 우리도 모아 둔다. 중간에 끊기면 이것만이 남는 전부다 —
|
|
683
|
+
// chatStream 은 끝까지 가야 message 를 주므로, 끊긴 순간에는 msg 가 비어 있다.
|
|
684
|
+
let 흘린것 = '';
|
|
685
|
+
/*
|
|
686
|
+
* 서버가 잠깐 막아 기다렸다 다시 부른 횟수는 /cost 와 deel run --json 이 본다.
|
|
687
|
+
* 다만 **정말 다시 불렀을 때** 센다 — 알림을 낸 뒤 기다리다 Ctrl+C 로 끊기면
|
|
688
|
+
* 다시 부른 것이 아니다. 그래서 셈은 미뤄 두고, 다음 소식이 오면 그때 더한다.
|
|
689
|
+
*/
|
|
690
|
+
let 미룬셈 = 0;
|
|
691
|
+
const 셈하기 = () => { session.usage.retries = (session.usage.retries ?? 0) + 미룬셈; 미룬셈 = 0; };
|
|
692
|
+
const 끝셈 = (err) => { if (err?.name === 'Aborted') 미룬셈 = 0; else 셈하기(); };
|
|
693
|
+
const askModel = async function* (maxTokens, think, { 한번에 = false } = {}) {
|
|
694
|
+
if (conn.streaming && !한번에) {
|
|
695
|
+
try {
|
|
696
|
+
for await (const ev of chatStream(conn, ask(maxTokens, think))) {
|
|
697
|
+
셈하기(); // 무슨 소식이든 왔다는 것은 앞의 다시 부름이 실제로 있었다는 뜻
|
|
698
|
+
if (ev.type === 'done') msg = ev.message;
|
|
699
|
+
else {
|
|
700
|
+
if (ev.type === 'content') 흘린것 += ev.text ?? '';
|
|
701
|
+
// 맞기 전에 비켜 준 것은 **다시 부른 것이 아니다.** 요청은 한 번만
|
|
702
|
+
// 나갔다. 이걸 같이 세면 `/cost` 의 「다시 부른 횟수」 와 `--json` 의
|
|
703
|
+
// retries 가 「서버가 N번 막았다」 고 말하는데, 서버는 안 막았다.
|
|
704
|
+
if (ev.type === 'backoff' && !ev.미리) 미룬셈 = 1;
|
|
705
|
+
yield ev;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch (err) { 끝셈(err); throw err; }
|
|
709
|
+
셈하기();
|
|
710
|
+
} else {
|
|
711
|
+
yield { type: 'waiting' };
|
|
712
|
+
/*
|
|
713
|
+
* 한 번에 받는 길은 await 하나라 알림이 흘러나오지 않는다. 그래서 부름을
|
|
714
|
+
* 띄워 두고, 알림이 오면 먼저 내보내고, 답이 오면 그때 끝낸다. 안 그러면
|
|
715
|
+
* 서버가 60초 기다리라 한 동안 화면에는 '생각 중' 만 떠 있다 — 사람은
|
|
716
|
+
* 모델이 느린 줄 알지만 실은 429 였다.
|
|
717
|
+
*/
|
|
718
|
+
const 편지 = 우편함((onBackoff) => chat(conn, { ...ask(maxTokens, think), onBackoff }));
|
|
719
|
+
for await (const 알림 of 편지.소식()) { 셈하기(); 미룬셈 = 1; yield 알림; }
|
|
720
|
+
try { msg = await 편지.부름; } catch (err) { 끝셈(err); throw err; }
|
|
721
|
+
셈하기();
|
|
722
|
+
if (msg.thinking) yield { type: 'thinking', text: msg.thinking };
|
|
723
|
+
if (msg.content) yield { type: 'content', text: msg.content };
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
yield* askModel(cap, level);
|
|
729
|
+
|
|
730
|
+
/*
|
|
731
|
+
* 대답이 잘렸다면 한 번은 다시 부른다.
|
|
732
|
+
*
|
|
733
|
+
* 전에는 '상한을 올릴 수 있을 때만' 다시 불렀다. 그런데 컨텍스트가 큰
|
|
734
|
+
* 모델에서는 아낀 상한과 풀어 준 상한이 둘 다 같은 천장(16,384)에 닿아서
|
|
735
|
+
* cap < full 이 거짓이 됐다. **큰 파일을 쓰는 바로 그 기계에서만** 안전망이
|
|
736
|
+
* 꺼져 있었던 것이다.
|
|
737
|
+
*
|
|
738
|
+
* 그리고 상한을 못 올리는 자리에서도 할 수 있는 일이 하나 더 있다 —
|
|
739
|
+
* 생각을 줄이는 것이다. 추론 토큰은 답과 **같은 예산**에서 나간다.
|
|
740
|
+
* 상한이 8,000인데 생각에 6,000을 쓰면 답에 쓸 수 있는 것은 2,000뿐이다.
|
|
741
|
+
* 그래서 다시 부를 때는 생각을 한 칸 낮춘다. 예산이 실제로 남는다.
|
|
742
|
+
*
|
|
743
|
+
* 둘 다 못 하면(이미 천장이고 생각도 꺼져 있으면) 다시 부르지 않는다.
|
|
744
|
+
* 같은 조건으로 또 부르면 같은 자리에서 또 잘린다 — 그건 그냥 낭비다.
|
|
745
|
+
* 그 경우는 아래 '살려 쓰기' 가 받는다.
|
|
746
|
+
*/
|
|
747
|
+
const full = Math.max(cap, fullCap(room));
|
|
748
|
+
const 낮춘생각 = level === 'off' || level === 'low' ? level : shiftLevel(level, -1);
|
|
749
|
+
let 마지막상한 = cap;
|
|
750
|
+
if (wasCut(msg) && (full > cap || 낮춘생각 !== level)) {
|
|
751
|
+
yield {
|
|
752
|
+
type: 'retry',
|
|
753
|
+
why: full > cap ? '대답이 상한에서 잘렸습니다' : '대답이 잘렸습니다 — 생각을 줄여 자리를 냅니다',
|
|
754
|
+
from: cap, to: full, think: 낮춘생각,
|
|
755
|
+
};
|
|
756
|
+
마지막상한 = full;
|
|
757
|
+
yield* askModel(full, 낮춘생각);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/*
|
|
761
|
+
* 우리가 할 수 있는 것을 다 했는데도 잘렸다 — 여기서 입을 다물면 안 된다.
|
|
762
|
+
*
|
|
763
|
+
* 상한도 올려 봤고 생각도 줄여 봤는데 여전히 잘렸으면, 남은 일은 사람
|
|
764
|
+
* 몫이다. 그런데 여태 그냥 조용히 끝냈다. 화면에는 중간에서 끊긴 답만
|
|
765
|
+
* 남는다 — 사람 눈에는 모델이 게을러서 대충 답한 것으로 보이니 같은 것을
|
|
766
|
+
* 다시 시키고, 같은 자리에서 또 잘린다. 몇 분씩 가는 로컬 모델에서
|
|
767
|
+
* 이 왕복은 비싸다.
|
|
768
|
+
*
|
|
769
|
+
* 안 되는 것은 **까닭과 손댈 자리를 같이** 말한다 (`/out`).
|
|
770
|
+
* 다시 부른 뒤에 보는 것이 중요하다. 부르기 전에 판정하면, 상한을 올려
|
|
771
|
+
* 멀쩡히 끝난 답에까지 경고가 붙는다 — 그러면 곧 아무도 안 읽는다.
|
|
772
|
+
*/
|
|
773
|
+
if (wasCut(msg)) {
|
|
774
|
+
yield {
|
|
775
|
+
type: 'capped',
|
|
776
|
+
cap: 마지막상한,
|
|
777
|
+
정한값: conn.maxTokens ?? conn.maxOut ?? null,
|
|
778
|
+
};
|
|
779
|
+
} else if (msg?.stopped === 말없이끝남) {
|
|
780
|
+
/*
|
|
781
|
+
* 상한 때문에 잘린 것과는 다른 사고다. 서버가 끝난 까닭을 한 번도 안
|
|
782
|
+
* 주고 흘려보내기를 멈췄다 — 중계 프록시가 몸통을 자르고 연결을 곱게
|
|
783
|
+
* 닫으면 이 모양이 된다. 상한을 올려 다시 부르는 것은 답이 아니라서
|
|
784
|
+
* capped 와 섞으면 엉뚱한 곳(/out)을 고치게 만든다.
|
|
785
|
+
*/
|
|
786
|
+
yield { type: 'cutoff' };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/*
|
|
790
|
+
* 빈 답을 성공으로 넘기지 않는다.
|
|
791
|
+
*
|
|
792
|
+
* 스트리밍을 무시하는 서버, HTML 오류 페이지를 200 으로 주는 프록시,
|
|
793
|
+
* 규격이 안 맞는 게이트웨이 — 이럴 때 content 가 조용히 '' 가 된다.
|
|
794
|
+
* 화면에는 `── 1.9초` 만 찍히고 끝난다. 오류도 없다. 몇 번을 다시 물어도
|
|
795
|
+
* 같으니, 사용자는 프로그램이 고장 난 줄 안다.
|
|
796
|
+
*
|
|
797
|
+
* 스트리밍을 껐다가 한 번 다시 부른다. 원인이 그쪽이면 그 자리에서 낫는다.
|
|
798
|
+
*/
|
|
799
|
+
if (빈답인가(msg) && conn.streaming) {
|
|
800
|
+
yield { type: 'retry', why: '빈 답이 왔습니다 — 스트리밍을 끄고 다시 부릅니다', from: cap, to: cap };
|
|
801
|
+
흘린것 = '';
|
|
802
|
+
yield* askModel(cap, level, { 한번에: true });
|
|
803
|
+
// 껐더니 되면 이 서버는 스트리밍이 안 맞는 것이다. 이번 세션은 계속 꺼 둔다.
|
|
804
|
+
if (!빈답인가(msg)) {
|
|
805
|
+
conn.streaming = false;
|
|
806
|
+
yield { type: 'note', text: '이 서버는 스트리밍 응답이 비어서 이번 세션은 꺼 두고 씁니다.' };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (빈답인가(msg)) {
|
|
810
|
+
session.본것?.본것('빈답');
|
|
811
|
+
ctx.배움?.모델본것(conn.model, '빈답');
|
|
812
|
+
yield {
|
|
813
|
+
type: 'error',
|
|
814
|
+
text: '서버가 빈 답을 보냈습니다.\n'
|
|
815
|
+
+ ' 글도 도구 호출도 없습니다. 대개 셋 중 하나입니다 —\n'
|
|
816
|
+
+ ' · 모델 이름이 이 서버에 없는 것 (deel status 로 확인)\n'
|
|
817
|
+
+ ' · 프록시가 중간에서 응답을 바꿔 놓는 것\n'
|
|
818
|
+
+ ' · 컨텍스트가 꽉 차 답할 자리가 없는 것 (/context 로 확인)',
|
|
819
|
+
};
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
} catch (err) {
|
|
823
|
+
if (err?.name === 'Aborted' || signal?.aborted) {
|
|
824
|
+
/*
|
|
825
|
+
* 중단해도 **여기까지 한 말은 남긴다.**
|
|
826
|
+
*
|
|
827
|
+
* 전에는 "여기까지는 대화에 남아 있으니 이어서 말씀하세요" 라고 해 놓고
|
|
828
|
+
* 실제로는 아무것도 안 남겼다. 그래서 "이어서 해줘" 라고 하면 모델이
|
|
829
|
+
* 방금 제가 한 말을 몰랐다. 안내가 거짓이면 안 하느니만 못하다.
|
|
830
|
+
*/
|
|
831
|
+
/*
|
|
832
|
+
* `msg` 가 있다고 **대화에 실린 것은 아니다.**
|
|
833
|
+
*
|
|
834
|
+
* 답을 미는 자리는 이 try 가 끝난 뒤다. 그런데 이 안에서 답을 받고도
|
|
835
|
+
* 한 번 더 부르는 길이 둘 있다 — 잘려서 다시 부르기, 빈 답이라 다시
|
|
836
|
+
* 부르기. 그 사이에 사람이 Ctrl+C 를 누르면 `msg` 는 차 있는데 아직
|
|
837
|
+
* 아무것도 안 실린 상태다. 그런데도 `kept: !!msg` 라 화면에는
|
|
838
|
+
* 「여기까지는 대화에 남아 있으니 이어서 말씀하세요」 가 떴다.
|
|
839
|
+
* 사람이 「이어서 해줘」 라고 하면 모델은 방금 제가 한 말을 모른다 —
|
|
840
|
+
* 바로 위 주석이 없애겠다고 적어 둔 그 고장이 모양만 바꿔 남아 있었다.
|
|
841
|
+
*
|
|
842
|
+
* 그래서 **실제로 실었는지**를 보고 말한다. 아직 안 실렸으면 여기서 싣는다.
|
|
843
|
+
*/
|
|
844
|
+
const 남길것 = 흘린것.trim();
|
|
845
|
+
let 실었나 = false;
|
|
846
|
+
if (msg) { session.push(assistantMessage(conn.kind, msg)); 실었나 = true; }
|
|
847
|
+
else if (남길것) { session.push({ role: 'assistant', content: 남길것 }); 실었나 = true; }
|
|
848
|
+
짝맞추기();
|
|
849
|
+
yield { type: 'aborted', steps, kept: 실었나 };
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/*
|
|
854
|
+
* 거절당했으면 그 말에서 배운다.
|
|
855
|
+
*
|
|
856
|
+
* "This model's maximum context length is 8192 tokens, however you requested 41003"
|
|
857
|
+
*
|
|
858
|
+
* 서버가 정답을 알려 준 것이다. 이 방식은 **처음 보는 서버에서도 통한다** —
|
|
859
|
+
* 규격을 몰라도 되고, 새 서버가 나와도 코드를 안 고쳐도 된다.
|
|
860
|
+
*
|
|
861
|
+
* 배운 값을 conn 에 적고 곧바로 다시 부른다. 사용자는 실패를 안 본다.
|
|
862
|
+
* 한 걸음에 한 번만 배운다 — 배웠는데도 또 거절당하면 다른 문제다.
|
|
863
|
+
*/
|
|
864
|
+
const 배운 = 배울것(err.serverMessage ?? err.message);
|
|
865
|
+
if (배운 && !배운적) {
|
|
866
|
+
배운적 = true;
|
|
867
|
+
if (배운.kind === 'ctx') {
|
|
868
|
+
conn.ctx = 배운.limit;
|
|
869
|
+
if (ctx.연결저장) { try { ctx.연결저장({ ctx: 배운.limit }); } catch {} }
|
|
870
|
+
} else {
|
|
871
|
+
conn.maxOut = 배운.limit;
|
|
872
|
+
if (conn.maxTokens && conn.maxTokens > 배운.limit) conn.maxTokens = 배운.limit;
|
|
873
|
+
if (ctx.연결저장) { try { ctx.연결저장({ maxTokens: 배운.limit }); } catch {} }
|
|
874
|
+
}
|
|
875
|
+
yield { type: 'learned', what: 배운.kind, limit: 배운.limit, asked: 배운.asked, from: 배운.text };
|
|
876
|
+
steps--; // 배우느라 쓴 걸음은 안 센다. 일은 아직 시작도 안 했다.
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// 숫자는 못 뽑았지만 '길어서' 인 것은 알겠으면, 줄여서 한 번 해 본다.
|
|
881
|
+
if (!배운적 && 길이문제인가(err.serverMessage ?? err.message) && (conn.ctx ?? 0) > 8192) {
|
|
882
|
+
배운적 = true;
|
|
883
|
+
const 줄인것 = Math.max(8192, Math.floor((conn.ctx ?? 32768) / 2));
|
|
884
|
+
conn.ctx = 줄인것;
|
|
885
|
+
yield { type: 'learned', what: 'ctx', limit: 줄인것, guessed: true, from: 짧게말(err.serverMessage ?? err.message) };
|
|
886
|
+
steps--;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/*
|
|
891
|
+
* ── 서버가 「그 칸은 안 받는다」 고 하면 카드를 고친다 ────────────────
|
|
892
|
+
*
|
|
893
|
+
* 규격을 하나하나 아는 방식으로는 못 따라간다. 그런데 서버는 거절할 때
|
|
894
|
+
* **무엇이 안 되는지 그대로 말해 준다** — "budget_tokens is not
|
|
895
|
+
* supported", "reasoning_effort must be one of: minimal, low, medium,
|
|
896
|
+
* high". 그 한 줄이면 짐작을 그만두고 사실로 갈아탈 수 있다.
|
|
897
|
+
*
|
|
898
|
+
* 걸음은 안 센다. 아직 일은 시작도 안 했고, 고친 값으로 다시 부르는
|
|
899
|
+
* 것이 이 자리의 전부다 (위 '배우기' 와 같은 셈법).
|
|
900
|
+
*/
|
|
901
|
+
const 전선고침 = 배울전선(err.serverMessage ?? err.message, conn.kind);
|
|
902
|
+
if (전선고침 && !전선배운적) {
|
|
903
|
+
전선배운적 = true;
|
|
904
|
+
conn.전선 = 카드고치기(conn.전선, 전선고침);
|
|
905
|
+
try { ctx.배움?.전선본것?.(conn.model, conn.base, 카드저장꼴(conn.전선), conn.kind); } catch { /* 못 적어도 이번 대화에는 먹는다 */ }
|
|
906
|
+
yield {
|
|
907
|
+
type: 'wire', 무엇: 전선고침.무엇, 값: 전선고침.값,
|
|
908
|
+
from: 짧게말(err.serverMessage ?? err.message),
|
|
909
|
+
};
|
|
910
|
+
steps--;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/*
|
|
915
|
+
* ── 자리가 다 찼으면, 턴을 죽이지 말고 비우고 이어 간다 ──────────────
|
|
916
|
+
*
|
|
917
|
+
* 여기가 사용자가 이름 대어 말한 자리다 — "current 가 다 차서 막힐 경우,
|
|
918
|
+
* 초기화되고 난 후에 다시 바로 작업할 수 있게".
|
|
919
|
+
*
|
|
920
|
+
* 여태 있었던 일: 서버가 「자리가 없다」고 거절한다 → 위에서 한계를
|
|
921
|
+
* 배우고 다시 부른다 → 접기가 걸음의 머리에서 도니 대개 여기서 낫는다.
|
|
922
|
+
* 그래도 안 맞으면(요약 자체가 크거나, 요약을 못 받았거나) 배운적이
|
|
923
|
+
* 이미 참이라 그대로 오류로 떨어졌다. 화면에는 붉은 줄 하나가 뜨고
|
|
924
|
+
* **턴이 죽는다.** 사람은 /clear 를 치고, 시킨 말을 다시 타이핑하고,
|
|
925
|
+
* 붙였던 파일을 다시 붙여야 한다. 한 걸음도 못 나아간 채로.
|
|
926
|
+
*
|
|
927
|
+
* 그래서 이 자리에서 **턴 안에서** 비우고 이어 간다. 버리는 것은 지나간
|
|
928
|
+
* 대화뿐이고, 잃으면 안 되는 것은 그대로 옮긴다 —
|
|
929
|
+
* · 시킨 말 원문과 남은 할 일 (못박을것)
|
|
930
|
+
* · 이번 턴에 이미 손댄 파일 이름
|
|
931
|
+
* 모델이 다음 걸음에서 보는 것은 「무엇을 시켰고 · 어디까지 했나」 다.
|
|
932
|
+
* 그거면 이어서 할 수 있다.
|
|
933
|
+
*
|
|
934
|
+
* 읽어 둔 파일 기억도 같이 지운다. 내용은 대화에서 사라졌는데 '이미
|
|
935
|
+
* 읽었다' 는 표만 남으면, 모델은 다시 안 읽고 기억에 없는 파일을
|
|
936
|
+
* 고치려 든다 — 비운 것보다 나쁘다.
|
|
937
|
+
*
|
|
938
|
+
* 한 턴에 한 번뿐이다. 비우고도 또 막히면 그건 자리 문제가 아니라
|
|
939
|
+
* 다른 탈이라, 끝없이 비우며 도는 대신 있는 그대로 오류로 말한다.
|
|
940
|
+
*/
|
|
941
|
+
const 자리문제 = 배운?.kind === 'ctx' || 길이문제인가(err.serverMessage ?? err.message);
|
|
942
|
+
if (자리문제 && !비운적) {
|
|
943
|
+
비운적 = true;
|
|
944
|
+
const 지운수 = session.messages.length;
|
|
945
|
+
const 손댄것 = [...손댄파일];
|
|
946
|
+
const 남은할일수 = (ctx.todos ?? []).filter((x) => x?.state !== 'done').length;
|
|
947
|
+
session.messages = [{
|
|
948
|
+
role: 'user',
|
|
949
|
+
content: `(자리가 모자라 앞선 대화 ${지운수}개를 비웠습니다. 필요한 파일은 다시 읽으세요.)\n`
|
|
950
|
+
+ (손댄것.length ? `이번 턴에 이미 손댄 파일: ${손댄것.join(', ')}\n` : '')
|
|
951
|
+
+ '\n' + 못박을것(session),
|
|
952
|
+
}];
|
|
953
|
+
// 내용은 없어졌는데 '읽었다' 는 표만 남으면 모델이 다시 안 읽는다.
|
|
954
|
+
try { session.filesRead?.clear?.(); session.파일기억?.잊기?.(); } catch { /* 없어도 그만 */ }
|
|
955
|
+
yield {
|
|
956
|
+
type: 'reset',
|
|
957
|
+
dropped: 지운수,
|
|
958
|
+
kept: { 요청: !!String(session.이번요청 ?? '').trim(), 할일: 남은할일수, 파일: 손댄것.length },
|
|
959
|
+
};
|
|
960
|
+
/*
|
|
961
|
+
* 배우기(위)와 달리 여기서는 걸음을 **안 돌려준다.**
|
|
962
|
+
*
|
|
963
|
+
* 돌려주면 걸음 수가 영영 안 올라간다. 그러면 위의 한 번 규칙이
|
|
964
|
+
* 어쩌다 무너졌을 때 걸음 상한도 못 잡아서 턴이 **영영 돈다** —
|
|
965
|
+
* 화면은 멈춘 채로 있고 사람은 왜 그런지 알 길이 없다. 붉은 줄
|
|
966
|
+
* 하나보다 훨씬 나쁘다. 한 걸음 값은 어차피 실제로 한 번 부르고
|
|
967
|
+
* 거절당한 값이라, 안 돌려주는 쪽이 사실에도 맞다.
|
|
968
|
+
*/
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/*
|
|
973
|
+
* 탈이 나도 **여기까지 흘러온 말은 남긴다.**
|
|
974
|
+
*
|
|
975
|
+
* 중단(위)에서는 이미 남기고 있었는데 오류에서는 안 남기고 있었다.
|
|
976
|
+
* 그런데 사람이 겪는 것은 오히려 이쪽이 잦다 — 200자를 받아 화면에
|
|
977
|
+
* 읽고 있는데 연결이 끊기면, 화면에는 그 200자가 그대로 보이는데
|
|
978
|
+
* 대화에는 한 글자도 안 들어간다. 그 상태에서 "이어서 해줘" 라고
|
|
979
|
+
* 하면 모델은 방금 제가 한 말을 모른다. 화면과 대화가 어긋나 있으면
|
|
980
|
+
* 사람은 그 뒤로 무엇을 믿어야 할지 알 수 없다.
|
|
981
|
+
*
|
|
982
|
+
* 반쪽인 것은 중단과 같은 표를 붙여 남긴다 — 온전한 답으로 읽히면
|
|
983
|
+
* 안 된다. 도구 호출만 있고 결과가 빈 자리도 같이 채운다(짝맞추기).
|
|
984
|
+
* 안 채우면 다음 요청에서 게이트웨이가 통째로 400 을 준다.
|
|
985
|
+
*/
|
|
986
|
+
// 중단 쪽과 같은 규칙이다 — 실었는지를 보고 말한다.
|
|
987
|
+
const 오류때남길것 = 흘린것.trim();
|
|
988
|
+
let 오류때실었나 = false;
|
|
989
|
+
if (msg) { session.push(assistantMessage(conn.kind, msg)); 오류때실었나 = true; }
|
|
990
|
+
else if (오류때남길것) { session.push({ role: 'assistant', content: 오류때남길것 }); 오류때실었나 = true; }
|
|
991
|
+
짝맞추기();
|
|
992
|
+
yield { type: 'error', text: err.message, kept: 오류때실었나 };
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/*
|
|
997
|
+
* usage 를 **안 준 응답**을 따로 센다.
|
|
998
|
+
*
|
|
999
|
+
* 0 을 더하면 합계는 그대로라 화면에 표가 안 난다. 그런데 그 부름은
|
|
1000
|
+
* 유료였고, 쓴 것을 우리가 모를 뿐이다. 「0 원 썼다」 로 적으면 그건
|
|
1001
|
+
* 지어낸 값이다 (backend/adapter.js 의 잰것).
|
|
1002
|
+
*/
|
|
1003
|
+
if (msg.usage?.잰것 === false) session.usage.못잰것 = (session.usage.못잰것 ?? 0) + 1;
|
|
1004
|
+
session.usage.in += msg.usage?.in ?? 0;
|
|
1005
|
+
session.usage.out += msg.usage?.out ?? 0;
|
|
1006
|
+
// 캐시가 얼마나 맞았나. 읽기와 쓰기를 따로 센다 — 「매번 쓰기만 하고 한
|
|
1007
|
+
// 번도 못 읽는」 것과 「잘 읽고 있는」 것은 완전히 다른 상태다.
|
|
1008
|
+
session.usage.cacheRead = (session.usage.cacheRead ?? 0) + (msg.usage?.cacheRead ?? 0);
|
|
1009
|
+
session.usage.cacheWrite = (session.usage.cacheWrite ?? 0) + (msg.usage?.cacheWrite ?? 0);
|
|
1010
|
+
/*
|
|
1011
|
+
* 답 토큰 중 **생각에 쓴 몫**. 알려 주는 창구에서만 값이 온다.
|
|
1012
|
+
*
|
|
1013
|
+
* 이 값이 `/think` 를 높게 잡아 둔 대가를 눈에 보이게 한다. 안 알려 주는
|
|
1014
|
+
* 창구에서는 0 으로 남고, 0 이면 `/cost` 가 줄 자체를 안 적는다 — 모르는
|
|
1015
|
+
* 것을 0 으로 적으면 「생각을 안 했다」 로 읽힌다.
|
|
1016
|
+
*/
|
|
1017
|
+
session.usage.reasoning = (session.usage.reasoning ?? 0) + (msg.usage?.reasoning ?? 0);
|
|
1018
|
+
session.usage.calls++;
|
|
1019
|
+
|
|
1020
|
+
/*
|
|
1021
|
+
* 방금 보낸 것이 실제로 몇 토큰이었는지 서버가 알려 줬다. 우리 추정과
|
|
1022
|
+
* 견줘서 배운다 — 여기가 유일하게 정답을 아는 자리다.
|
|
1023
|
+
*
|
|
1024
|
+
* 답을 push 하기 **전**이어야 한다. 지금 이력이 곧 방금 보낸 프롬프트다.
|
|
1025
|
+
*/
|
|
1026
|
+
/*
|
|
1027
|
+
* 견줄 값은 `usage.in` 이 아니라 **이번에 보낸 프롬프트 전체**다.
|
|
1028
|
+
* 규격마다 `in` 이 세는 것이 다르다 — 까닭은 backend/adapter.js 의 보낸토큰.
|
|
1029
|
+
*/
|
|
1030
|
+
const 보낸것 = 보낸토큰(conn.kind, msg.usage);
|
|
1031
|
+
session.usage.prompt = (session.usage.prompt ?? 0) + 보낸것;
|
|
1032
|
+
const 새보정 = session.배운다?.(보낸것);
|
|
1033
|
+
// 배운 배수를 디스크에도 남긴다. 다음에 켤 때 이 값으로 시작한다.
|
|
1034
|
+
if (새보정) ctx.배움?.보정본것(conn.model, 새보정);
|
|
1035
|
+
|
|
1036
|
+
session.push(assistantMessage(conn.kind, msg));
|
|
1037
|
+
|
|
1038
|
+
/*
|
|
1039
|
+
* ── 안전 판정으로 거절당했다. 여기서 멈춘다 ────────────────────────
|
|
1040
|
+
*
|
|
1041
|
+
* `stop_reason: 'refusal'` 은 **답이 짧은 것이 아니라 안 하겠다는 것**이다.
|
|
1042
|
+
* 그런데 이 답에는 대개 도구 호출이 없어서, 아래 `밀어줄까` 가 「읽기만
|
|
1043
|
+
* 하고 끝내려 한다」 로 읽고 한 번 더 밀었다. 밀어도 판정은 같으니 또
|
|
1044
|
+
* 거절이고, 그게 걸음 수만큼 되풀이됐다.
|
|
1045
|
+
*
|
|
1046
|
+
* 그 되풀이가 곧 요금이다 — 한 번 거절당할 요청 하나가 열 번 나가고,
|
|
1047
|
+
* 그 열 번이 분당 한도를 밀어 올려 그 다음 진짜 요청이 429 를 받는다.
|
|
1048
|
+
* 「호출 한도 초과」 가 여기서도 만들어졌다.
|
|
1049
|
+
*
|
|
1050
|
+
* 그래서 거절은 되밀지 않고 그 자리에서 끝낸다. 사람이 무엇을 다시
|
|
1051
|
+
* 칠지는 사람이 정할 일이지, 같은 말을 열 번 더 보내서 알아낼 일이 아니다.
|
|
1052
|
+
*/
|
|
1053
|
+
if (msg.거절) {
|
|
1054
|
+
/*
|
|
1055
|
+
* 나가기 전에 짝을 맞춘다.
|
|
1056
|
+
*
|
|
1057
|
+
* 거절에는 도구 호출이 **대개** 없다. 대개지 늘이 아니다 — 흘려받기는
|
|
1058
|
+
* `tool_use` 블록을 먼저 열어 두고 맨 끝의 `stop_reason` 에서 거절을
|
|
1059
|
+
* 알려 주는 창구가 있고(adapter.js 의 흡수), 그러면 부름만 있고 결과가
|
|
1060
|
+
* 없는 채로 대화가 끝난다. 그 상태로 다음 말을 보내면 서버가 대화 전체를
|
|
1061
|
+
* 400 으로 돌려보낸다 — 거절 한 번이 그 뒤의 세션을 통째로 못 쓰게 만든다.
|
|
1062
|
+
*
|
|
1063
|
+
* 중단·오류에서 하던 것과 같은 일이다. 나가는 문마다 따로 기억하지 않게
|
|
1064
|
+
* 여기서도 부른다.
|
|
1065
|
+
*/
|
|
1066
|
+
짝맞추기('모델이 거절해 실행하지 않았습니다.');
|
|
1067
|
+
yield {
|
|
1068
|
+
type: 'refusal',
|
|
1069
|
+
왜: String(msg.거절?.type ?? 'refusal'),
|
|
1070
|
+
text: msg.content,
|
|
1071
|
+
steps,
|
|
1072
|
+
files: 마무리(),
|
|
1073
|
+
};
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (!msg.toolCalls?.length) {
|
|
1078
|
+
const 왜 = 밀어줄까(msg.content);
|
|
1079
|
+
if (왜) {
|
|
1080
|
+
민적 = true;
|
|
1081
|
+
session.push({ role: 'user', content: 되미는말() });
|
|
1082
|
+
yield { type: 'nudge', why: 왜, text: msg.content };
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const 빠진 = 빠뜨린것(msg.content);
|
|
1086
|
+
if (빠진.length && !빠뜨림민적) {
|
|
1087
|
+
빠뜨림민적 = true;
|
|
1088
|
+
session.push({ role: 'user', content: 빠졌다는말(빠진, { 영어: 지시말() === 'en' }) });
|
|
1089
|
+
yield { type: 'nudge', why: '요청누락', 빠진, text: msg.content };
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
// 밀고도 그대로면 조용히 넘어가지 않는다. 사람이 알아야 다음을 정한다.
|
|
1093
|
+
yield { type: 'done', steps, text: msg.content, files: 마무리(), 빠진 };
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// 도구를 돌린다. 읽기만 하는 것들이 이어지면 한꺼번에, 나머지는 하나씩.
|
|
1098
|
+
lastToolFailed = false;
|
|
1099
|
+
const 거절 = (call, note) => {
|
|
1100
|
+
lastToolFailed = true;
|
|
1101
|
+
session.push(toolMessage(conn.kind, { callId: call.id, name: call.name, content: note }));
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
/*
|
|
1105
|
+
* 이번 턴에 Read 로 연 그림들.
|
|
1106
|
+
*
|
|
1107
|
+
* 도구 결과가 다 들어간 **뒤에** 한꺼번에 붙인다. 하나 읽을 때마다 바로
|
|
1108
|
+
* 붙이면 도구 결과 사이에 사람 말이 끼어드는데, 그러면 뒤에 오는 도구
|
|
1109
|
+
* 결과들이 짝을 잃는다 — 게이트웨이가 통째로 400 을 준다.
|
|
1110
|
+
*/
|
|
1111
|
+
const 붙일그림 = [];
|
|
1112
|
+
|
|
1113
|
+
for (const 덩어리 of 묶기(msg.toolCalls)) {
|
|
1114
|
+
// 돌리는 중에 끊었다면, 남은 것은 실행하지 않고 결과 자리만 채운다.
|
|
1115
|
+
// 자리를 비우면 짝이 깨져 다음에 이어할 수 없다.
|
|
1116
|
+
if (signal?.aborted) {
|
|
1117
|
+
for (const call of 덩어리.calls) {
|
|
1118
|
+
session.push(toolMessage(conn.kind, {
|
|
1119
|
+
callId: call.id, name: call.name,
|
|
1120
|
+
content: '사용자가 중단했습니다. 실행하지 않았습니다.',
|
|
1121
|
+
}));
|
|
1122
|
+
}
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// 먼저 하나씩 걸러 낸다 — 물어보는 것도 여기서. 실제 실행은 통과한 것만.
|
|
1127
|
+
const 실행할것 = [];
|
|
1128
|
+
for (const call of 덩어리.calls) {
|
|
1129
|
+
// 인자가 잘려 온 호출. 도구에 넘기면 안 된다 —
|
|
1130
|
+
// 도구는 '경로가 비었다' 같은, 원인과 상관없는 말을 하게 되고
|
|
1131
|
+
// 모델은 고칠 게 없다고 보고 똑같이 다시 시도한다. 그래서 끝없이 돈다.
|
|
1132
|
+
if (call.argsBroken) {
|
|
1133
|
+
session.본것?.본것('잘린인자');
|
|
1134
|
+
ctx.배움?.모델본것(conn.model, '잘린인자');
|
|
1135
|
+
// ── 버리기 전에, 건질 수 있는지 먼저 본다 ──────────────────────
|
|
1136
|
+
//
|
|
1137
|
+
// 잘린 JSON 안에는 이미 받아 놓은 내용이 들어 있다. 경로도 대개 온전하다.
|
|
1138
|
+
// 그걸 통째로 버리고 '다시 하세요' 라고 하면, 모델은 똑같이 다시 보내고
|
|
1139
|
+
// 똑같은 자리에서 또 잘린다. 그 되풀이가 71초와 파일 0개를 만들었다.
|
|
1140
|
+
//
|
|
1141
|
+
// 건져서 **실제로 쓰면** 다음 호출은 나머지만 보내면 된다 — 잘릴 일이 없다.
|
|
1142
|
+
// 그러니 이건 오류 처리가 아니라 일을 끝내는 길이다.
|
|
1143
|
+
//
|
|
1144
|
+
// Write 만 살린다. Edit 의 old_string 이 반쪽이면 엉뚱한 자리를 고치거나
|
|
1145
|
+
// 안 맞아 실패한다. 그건 안 하느니만 못하다.
|
|
1146
|
+
const 건진것 = (call.name === 'Write' || call.name === 'Append') ? 살린쓰기(call.rawArgs) : null;
|
|
1147
|
+
if (건진것) {
|
|
1148
|
+
const 도구 = call.name === 'Append' ? TOOLS.Append : TOOLS.Write;
|
|
1149
|
+
let r;
|
|
1150
|
+
try { r = await 도구.run({ file_path: 건진것.path, content: 건진것.content }, ctx); }
|
|
1151
|
+
catch (err) { r = { error: err.message }; }
|
|
1152
|
+
|
|
1153
|
+
if (!r.error) {
|
|
1154
|
+
/*
|
|
1155
|
+
* 나아가고 있나 — 이건 **파일 크기로 판단한다.** 말이 아니라 사실로.
|
|
1156
|
+
*
|
|
1157
|
+
* 살려 쓰기가 성공했다는 것만으로 나아간 것이라고 보면 안 된다.
|
|
1158
|
+
* 모델이 똑같이 잘린 호출을 계속 보내면 매번 같은 내용을 같은 자리에
|
|
1159
|
+
* 다시 쓴다 — 성공은 하는데 파일은 그대로다. 그건 헛도는 것이다.
|
|
1160
|
+
* 그때 반복 감지를 꺼 버리면 걸음 수를 다 쓸 때까지 안 멈춘다.
|
|
1161
|
+
*
|
|
1162
|
+
* 파일이 지난번보다 커졌을 때만 나아간 것으로 본다.
|
|
1163
|
+
*/
|
|
1164
|
+
const 지금크기 = r.changed ? (파일현황(r.changed).bytes ?? 0) : 0;
|
|
1165
|
+
const 앞크기 = 살린크기.get(r.changed) ?? -1;
|
|
1166
|
+
if (지금크기 > 앞크기) {
|
|
1167
|
+
살린크기.set(r.changed, 지금크기);
|
|
1168
|
+
나아감();
|
|
1169
|
+
} else if (막힘셈(call, '살려 써도 파일이 안 자람')) {
|
|
1170
|
+
멈출까 = '같은 내용이 계속 잘려서 옵니다 — 더 짧게 나눠 보내야 합니다';
|
|
1171
|
+
}
|
|
1172
|
+
const note = `인자가 잘려서, **받은 데까지만 파일에 썼습니다.**\n`
|
|
1173
|
+
+ ` ${건진것.path} · ${건진것.lines}줄까지 저장됨\n`
|
|
1174
|
+
+ ` 마지막 줄: ${건진것.lastLine}\n`
|
|
1175
|
+
+ ` 이어서 Append 로 **그 다음 줄부터** 보내세요. 앞부분은 다시 보내지 마세요 —\n`
|
|
1176
|
+
+ ` 다시 보내면 또 같은 자리에서 잘립니다. 한 번에 300줄 안쪽으로 끊어 보내세요.`;
|
|
1177
|
+
session.push(toolMessage(conn.kind, { callId: call.id, name: call.name, content: note }));
|
|
1178
|
+
if (r.changed) { session.noteChange(r.changed, r.diff); 손댄파일.add(r.changed); }
|
|
1179
|
+
ctx.audit.tool(call.name, { file_path: 건진것.path }, { summary: `잘린 것을 살려 ${건진것.lines}줄 씀` });
|
|
1180
|
+
yield {
|
|
1181
|
+
type: 'tool', name: call.name, args: { file_path: 건진것.path },
|
|
1182
|
+
result: { ...r, summary: `${건진것.lines}줄 · 잘린 데까지`, warn: `잘린 데까지만 썼습니다 — ${건진것.lines}줄` },
|
|
1183
|
+
showLabel: true,
|
|
1184
|
+
};
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
// 살려 쓰는 것마저 실패했으면(범위 밖 경로 등) 아래 평범한 거절로 내려간다.
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const note = `${call.name} 의 인자(JSON)가 중간에서 잘려 읽을 수 없습니다.`
|
|
1191
|
+
+ ' 한 번에 보내기엔 내용이 너무 큽니다.\n'
|
|
1192
|
+
+ ' 통째로 다시 보내지 마세요 — 또 같은 자리에서 잘립니다.\n'
|
|
1193
|
+
+ ' 파일을 만드는 중이라면: Write 로 앞부분만 300줄 안쪽으로 만들고,'
|
|
1194
|
+
+ ' 나머지는 Append 를 여러 번 불러 이어 붙이세요.';
|
|
1195
|
+
거절(call, note);
|
|
1196
|
+
// 감사기록에 남긴다. 자율 실행을 사내에 설득할 때 근거가 되는 파일이라,
|
|
1197
|
+
// '스스로 안 한 일' 도 남아야 한다.
|
|
1198
|
+
ctx.audit.blocked('인자가 잘려 실행하지 않음', `${call.name} · ${String(call.rawArgs ?? '').length}자`);
|
|
1199
|
+
if (막힘셈(call, '인자잘림')) 멈출까 = '같은 도구 호출이 계속 잘립니다';
|
|
1200
|
+
yield { type: 'tool', name: call.name, args: {}, result: { error: '인자가 잘렸습니다 — 한 번에 보내기엔 너무 큽니다' }, showLabel: true };
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (!TOOLS[call.name]) {
|
|
1205
|
+
거절(call, `모르는 도구입니다. 쓸 수 있는 것: ${Object.keys(TOOLS).join(', ')}`);
|
|
1206
|
+
// 이것도 세야 한다. 없는 이름을 계속 부르며 걸음 수를 다 쓰는 길이 있었다 —
|
|
1207
|
+
// 71초 증상과 겉모습이 똑같은데 문만 다르다. 아래 거부·중복도 마찬가지다.
|
|
1208
|
+
if (막힘셈(call, '모르는 도구')) 멈출까 = `${call.name} 은 없는 도구인데 계속 부르고 있습니다`;
|
|
1209
|
+
yield { type: 'tool', name: call.name, args: call.args, result: { error: '모르는 도구' }, showLabel: true };
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// 변경성 명령은 실패해도 다시 실행하지 않는다 — 두 번 돌면 사고다.
|
|
1214
|
+
if (call.name === 'Bash' && isMutating(call.args?.command)) {
|
|
1215
|
+
const key = String(call.args.command).trim();
|
|
1216
|
+
if (attempted.has(key)) {
|
|
1217
|
+
const note = '같은 변경성 명령을 다시 실행하지 않습니다. 두 번 실행되면 사고가 납니다.';
|
|
1218
|
+
거절(call, note);
|
|
1219
|
+
if (막힘셈(call, '두 번째 실행 거부')) 멈출까 = '같은 명령을 계속 다시 부르고 있습니다';
|
|
1220
|
+
yield { type: 'tool', name: call.name, args: call.args, result: { error: note }, showLabel: true };
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
attempted.add(key);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/*
|
|
1227
|
+
* 적어 둔 규칙이 모드보다 먼저다 (safety/policy.js).
|
|
1228
|
+
*
|
|
1229
|
+
* 금지 — 물어보지도 않는다. 물어보는 것은 막는 것이 아니다.
|
|
1230
|
+
* `npm test` 에 스무 번 y 를 친 손은 스물한 번째도 친다.
|
|
1231
|
+
* 허락 — 모드가 뭐든 안 묻는다.
|
|
1232
|
+
* 없으면 예전 그대로 모드가 정한다.
|
|
1233
|
+
*
|
|
1234
|
+
* 막았으면 **어디에 적힌 규칙인지까지** 말한다. 그 말이 없으면 사람은
|
|
1235
|
+
* 제 설정을 고칠 수도, 관리자에게 무엇을 풀어 달라고 할 수도 없다.
|
|
1236
|
+
*/
|
|
1237
|
+
const 판정 = 어떻게할까(ctx.규칙들, call.name, call.args);
|
|
1238
|
+
if (판정.답 === 'deny') {
|
|
1239
|
+
ctx.audit?.blocked?.('규칙으로 금지됨', `${판정.출처}: ${판정.규칙}`);
|
|
1240
|
+
거절(call, `${판정.출처}에 적힌 규칙 ${판정.규칙} 으로 막혀 있습니다.`
|
|
1241
|
+
+ ' 이 방법은 쓸 수 없습니다 — 다른 길을 찾거나, 왜 필요한지 사용자에게 말하세요.'
|
|
1242
|
+
+ ' 같은 것을 다시 부르지 마세요.');
|
|
1243
|
+
if (막힘셈(call, '규칙 금지')) 멈출까 = '규칙으로 막힌 것을 계속 다시 부르고 있습니다';
|
|
1244
|
+
yield {
|
|
1245
|
+
type: 'tool', name: call.name, args: call.args, showLabel: true,
|
|
1246
|
+
result: { error: `막힘 — ${판정.출처}의 ${판정.규칙}` },
|
|
1247
|
+
};
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// 모드에 따라 물어본다. 기본(auto)은 안 묻고 되돌리기로 대응한다.
|
|
1252
|
+
const needsOk = 판정.답 === 'allow' ? false : session.mode === 'strict'
|
|
1253
|
+
? ['Write', 'Edit', 'Bash'].includes(call.name)
|
|
1254
|
+
: session.mode === 'confirm'
|
|
1255
|
+
? (call.name === 'Bash' && isMutating(call.args?.command))
|
|
1256
|
+
: false;
|
|
1257
|
+
if (needsOk && ctx.confirm) {
|
|
1258
|
+
const ok = await ctx.confirm(call.name, call.args);
|
|
1259
|
+
if (!ok) {
|
|
1260
|
+
거절(call, '사용자가 거부했습니다. 다른 방법을 찾거나 이유를 물어보세요.');
|
|
1261
|
+
// 거부를 못 알아듣고 같은 것을 계속 물으면 사람만 계속 n 을 치게 된다.
|
|
1262
|
+
if (막힘셈(call, '사용자 거부')) 멈출까 = '거부하셨는데도 같은 것을 계속 물어보고 있습니다';
|
|
1263
|
+
yield { type: 'tool', name: call.name, args: call.args, result: { error: '거부됨' }, showLabel: true };
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
실행할것.push(call);
|
|
1268
|
+
}
|
|
1269
|
+
if (!실행할것.length) continue;
|
|
1270
|
+
|
|
1271
|
+
/*
|
|
1272
|
+
* ── 하위 작업 ─────────────────────────────────────────────────────
|
|
1273
|
+
*
|
|
1274
|
+
* 여기서 가로챈다. 평범한 도구 경로(runTool)로 보내면 안 된다.
|
|
1275
|
+
*
|
|
1276
|
+
* 도구는 `{content, summary}` 만 돌려줄 뿐 이벤트를 못 흘린다. 하위
|
|
1277
|
+
* 작업은 몇 분씩 도는 일이라, 그 길로 보내면 그동안 화면이 완전히
|
|
1278
|
+
* 죽는다 — 사람은 멈춘 건지 도는 건지 알 수 없어 Ctrl+C 를 누른다.
|
|
1279
|
+
* 그래서 여기서 직접 돌리고, 하위 루프의 이벤트를 깊이만 붙여 그대로
|
|
1280
|
+
* 위로 올려보낸다. 화면은 그걸 한 단 들여 그린다 (ui/screen.js).
|
|
1281
|
+
*
|
|
1282
|
+
* Task 는 읽기전용이 아니므로 묶기()가 언제나 혼자 한 덩어리로 낸다.
|
|
1283
|
+
*/
|
|
1284
|
+
if (실행할것.length === 1 && 실행할것[0].name === 'Task') {
|
|
1285
|
+
const call = 실행할것[0];
|
|
1286
|
+
const 목적 = String(call.args?.purpose ?? call.args?.목적 ?? '').trim() || '이름 없는 작업';
|
|
1287
|
+
const 할일 = String(call.args?.task ?? call.args?.할일 ?? '').trim();
|
|
1288
|
+
|
|
1289
|
+
// 할 일이 비면 하위는 아무것도 모른 채로 시작한다. 하위는 이 대화를
|
|
1290
|
+
// 못 보므로, 여기서 통과시키면 걸음만 태우고 빈손으로 돌아온다.
|
|
1291
|
+
if (!할일) {
|
|
1292
|
+
const note = '할일 이 비어 있습니다. 하위 작업은 지금 대화를 볼 수 없으니,'
|
|
1293
|
+
+ ' 필요한 배경·정한 것·파일 경로와 "무엇이 끝나면 다 된 것인지" 를 할일 에 다 적어 주세요.';
|
|
1294
|
+
거절(call, note);
|
|
1295
|
+
if (막힘셈(call, '할일 비었음')) 멈출까 = '하위 작업을 할 일 없이 계속 부르고 있습니다';
|
|
1296
|
+
yield { type: 'tool', name: 'Task', args: call.args, result: { error: '할일이 비었습니다' }, showLabel: true };
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const 자식모드 = 하위모드(call.args?.mode ?? call.args?.모드, 모드.id);
|
|
1301
|
+
|
|
1302
|
+
/*
|
|
1303
|
+
* ── 다른 모델에게 떼어 주기 ─────────────────────────────────────
|
|
1304
|
+
*
|
|
1305
|
+
* 안 적으면 지금 쓰는 것 그대로다. 적었으면 **사람이 설정에 적어 둔
|
|
1306
|
+
* 프로필**에서만 찾는다 — 모델이 주소를 지어내면 그 자리로 나갈 뻔하고,
|
|
1307
|
+
* 그건 이 프로그램이 하는 약속을 그 자리에서 깨는 것이다.
|
|
1308
|
+
*
|
|
1309
|
+
* 못 찾으면 조용히 지금 모델로 돌지 않는다. 시킨 쪽은 작은 모델에게
|
|
1310
|
+
* 넘긴 줄 알고 창을 아끼고 있는데 실제로는 큰 모델이 다 받는 셈이라,
|
|
1311
|
+
* 원인을 못 찾는 쪽으로 어긋난다. 무엇이 있는지 적어 되돌려 준다.
|
|
1312
|
+
*/
|
|
1313
|
+
let 자식conn = conn;
|
|
1314
|
+
let 자리닫기 = null;
|
|
1315
|
+
let 모델알림 = null;
|
|
1316
|
+
const 부른모델 = String(call.args?.model ?? call.args?.모델 ?? '').trim();
|
|
1317
|
+
if (부른모델) {
|
|
1318
|
+
const 찾음 = 프로필찾기(부른모델);
|
|
1319
|
+
if (!찾음.ok) {
|
|
1320
|
+
const 있는것 = 목록보기().map((x) => x.id).join(' · ') || '(설정에 프로필이 없습니다)';
|
|
1321
|
+
거절(call, `${찾음.why}. 쓸 수 있는 이름: ${있는것}`);
|
|
1322
|
+
yield { type: 'tool', name: 'Task', args: call.args, result: { error: 찾음.why }, showLabel: true };
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
const 되나 = 쓸수있나(찾음.prof);
|
|
1326
|
+
if (!되나.ok) {
|
|
1327
|
+
거절(call, `${부른모델} 은 지금 못 씁니다 — ${되나.why}`);
|
|
1328
|
+
yield { type: 'tool', name: 'Task', args: call.args, result: { error: 되나.why }, showLabel: true };
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
자식conn = 연결만들기(찾음.prof);
|
|
1332
|
+
/*
|
|
1333
|
+
* 떼어 준 자리에도 전선 카드를 달아 준다 (backend/wire.js).
|
|
1334
|
+
*
|
|
1335
|
+
* 여기를 빼면 다른 모델로 넘긴 일만 캐시도 안 걸리고 생각 칸도
|
|
1336
|
+
* 짐작으로 나간다 — 그리고 그건 화면 어디에도 안 나타난다.
|
|
1337
|
+
* 대화 이름은 부모 것을 그대로 물려준다: 사람이 시킨 것은 하나다.
|
|
1338
|
+
*/
|
|
1339
|
+
전선붙이기(자식conn, ctx.배움);
|
|
1340
|
+
자식conn.세션이름 = conn.세션이름 ?? null;
|
|
1341
|
+
모델알림 = 알릴말(conn, 자식conn);
|
|
1342
|
+
// 그 일이 도는 동안만 연다. 끝나면 반드시 닫는다 — 아래 finally.
|
|
1343
|
+
if (모델알림.다른자리) 자리닫기 = allowTemporarily(자식conn.base);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const 자식 = new Session(자식conn, {
|
|
1347
|
+
root: session.root,
|
|
1348
|
+
// 승인 방식은 그대로 물려준다. 하위가 승인을 우회하면 strict 가 거짓말이 된다.
|
|
1349
|
+
mode: session.mode,
|
|
1350
|
+
work: 자식모드,
|
|
1351
|
+
level: session.level,
|
|
1352
|
+
think: session.think,
|
|
1353
|
+
effort: session.effort,
|
|
1354
|
+
web: session.web,
|
|
1355
|
+
// 부모보다 적게 준다 (budget.js). 사람이 직접 정한 값이 있으면 그 절반.
|
|
1356
|
+
maxSteps: session.stepsSet
|
|
1357
|
+
? Math.max(4, Math.floor(session.maxSteps / 2))
|
|
1358
|
+
// 걸음 수는 **하위가 쓸 창**으로 잰다. 부모 창으로 재면, 작은 모델에게
|
|
1359
|
+
// 떼어 준 일이 제 창보다 큰 걸음 수를 받아 중간에 창이 찬 채로 돈다.
|
|
1360
|
+
: 하위걸음수(자식모드, 자식conn.ctx),
|
|
1361
|
+
});
|
|
1362
|
+
// 스킬·명령·기억은 부모가 켤 때 한 번 찾아 든 것이다. 하위도 같은 것을 본다.
|
|
1363
|
+
자식.skills = session.skills;
|
|
1364
|
+
자식.commands = session.commands;
|
|
1365
|
+
자식.plugins = session.plugins;
|
|
1366
|
+
자식.memory = session.memory;
|
|
1367
|
+
자식.도구제한 = 자식도구;
|
|
1368
|
+
|
|
1369
|
+
/*
|
|
1370
|
+
* 여닫는 줄에는 깊이를 안 붙인다.
|
|
1371
|
+
*
|
|
1372
|
+
* 이벤트가 들고 다니는 깊이는 **그 이벤트를 낸 자리 기준**이고, 위로
|
|
1373
|
+
* 한 겹 올라갈 때마다 1씩 붙어 절대 깊이가 된다. 그런데 이 줄은 하위가
|
|
1374
|
+
* 낸 것이 아니라 **하위를 떼어 주는 쪽**이 내는 경계선이다. 여기서
|
|
1375
|
+
* 미리 +1 을 해 두면 두 겹째에서 1이 두 번 붙어 3이 된다.
|
|
1376
|
+
* 경계선은 부모 자리에 그어야 어디부터가 떼어 낸 일인지 보인다.
|
|
1377
|
+
*/
|
|
1378
|
+
/*
|
|
1379
|
+
* 다른 모델을 쓰면 **어디로 나가는지까지** 적는다.
|
|
1380
|
+
*
|
|
1381
|
+
* 조용히 여는 것이 제일 나쁘다. 상태줄의 ⌂ 는 이 세션의 연결을 보고
|
|
1382
|
+
* 있어서 하위가 딴 데로 나가는 것을 모른다. 그러니 이 줄과 감사기록이
|
|
1383
|
+
* 그 사실을 남기는 유일한 자리다.
|
|
1384
|
+
*/
|
|
1385
|
+
yield {
|
|
1386
|
+
type: 'task_start', 목적, 모드: 자식모드, steps: 자식.maxSteps,
|
|
1387
|
+
모델: 모델알림?.말 ?? null, 밖으로: 모델알림?.밖으로 ?? false,
|
|
1388
|
+
};
|
|
1389
|
+
ctx.audit.tool('Task', { 목적, 모드: 자식모드, 모델: 모델알림?.말 ?? null },
|
|
1390
|
+
{ summary: `하위 작업 시작 (${깊이 + 1}겹)${모델알림 ? ` · ${모델알림.말}` : ''}` });
|
|
1391
|
+
|
|
1392
|
+
let 끝 = null;
|
|
1393
|
+
try {
|
|
1394
|
+
/*
|
|
1395
|
+
* ctx 를 **그대로** 넘긴다. 새로 만들면 안 된다.
|
|
1396
|
+
*
|
|
1397
|
+
* scope 를 새로 만들면 "이 폴더 밖은 못 건드린다" 가 하위에서 거짓이
|
|
1398
|
+
* 되고, history 를 새로 만들면 하위가 고친 파일이 `/undo` 에 안 잡히며,
|
|
1399
|
+
* audit 을 새로 만들면 하위가 한 일이 감사기록에서 통째로 사라진다.
|
|
1400
|
+
* 셋 다 "자율 실행을 사내에 설득할 때 근거가 되는" 것들이다.
|
|
1401
|
+
*/
|
|
1402
|
+
for await (const ev of run(자식, ctx, 할일, { signal, 깊이: 깊이 + 1 })) {
|
|
1403
|
+
// 하위의 '끝' 은 부모의 끝이 아니다. 여기서 삼키고 요약으로 바꾼다 —
|
|
1404
|
+
// 그대로 올리면 화면이 이번 턴이 다 끝난 줄 알고 입력 상자를 세운다.
|
|
1405
|
+
if (ev.type === 'done' || ev.type === 'limit' || ev.type === 'stuck' || ev.type === 'aborted') {
|
|
1406
|
+
끝 = ev;
|
|
1407
|
+
break;
|
|
1408
|
+
}
|
|
1409
|
+
yield { ...ev, depth: (ev.depth ?? 0) + 1 };
|
|
1410
|
+
}
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
// 하위가 터져도 부모 턴은 살린다. 무엇 때문에 터졌는지는 요약에 실린다.
|
|
1413
|
+
끝 = { type: 'stuck', why: String(err?.message ?? err), steps: 0, files: [] };
|
|
1414
|
+
} finally {
|
|
1415
|
+
// 잠깐 열어 둔 자리는 무슨 일이 있어도 닫는다. 안 닫으면 그 세션이
|
|
1416
|
+
// 끝날 때까지 그 주소가 열린 채로 남는다 — '한 자리만 연다' 가 깨진다.
|
|
1417
|
+
try { 자리닫기?.(); } catch { /* 닫다 터져도 이번 턴은 이어간다 */ }
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/*
|
|
1421
|
+
* 하위가 건드린 파일은 부모의 것이기도 하다. 턴 끝 목록에 같이 올린다.
|
|
1422
|
+
*
|
|
1423
|
+
* `/diff` 쪽은 따로 안 챙겨도 된다 — 하위의 도구 이벤트를 그대로 위로
|
|
1424
|
+
* 올려보내므로, repl.js 의 도구 처리가 부모 세션에 그대로 적어 넣는다.
|
|
1425
|
+
* 여기서 또 합치면 같은 변경이 두 번 세어진다.
|
|
1426
|
+
*/
|
|
1427
|
+
for (const f of 끝?.files ?? []) if (f?.path) 손댄파일.add(f.path);
|
|
1428
|
+
// 하위가 쓴 토큰·시간도 이번 턴의 셈에 들어가야 한다. 안 그러면 /context 가 거짓말을 한다.
|
|
1429
|
+
session.usage.in += 자식.usage.in;
|
|
1430
|
+
session.usage.out += 자식.usage.out;
|
|
1431
|
+
/*
|
|
1432
|
+
* 새로 생긴 네 칸도 같이 더한다.
|
|
1433
|
+
*
|
|
1434
|
+
* 안 더하면 하위가 쓴 것이 통째로 사라진다. 화면이 전부
|
|
1435
|
+
* `usage.prompt || usage.in` 을 보기 때문이다 — 부모가 한 번이라도
|
|
1436
|
+
* 부르면 prompt 가 0 이 아니라서 `|| in` 이 영영 안 걸리고, 그러면
|
|
1437
|
+
* 하위에 다 맡긴 턴이 `↑0` 으로 찍힌다. 바로 위 주석이 「안 그러면
|
|
1438
|
+
* /context 가 거짓말을 한다」 고 적어 둔 그 자리다.
|
|
1439
|
+
*/
|
|
1440
|
+
session.usage.prompt = (session.usage.prompt ?? 0) + (자식.usage.prompt ?? 0);
|
|
1441
|
+
session.usage.cacheRead = (session.usage.cacheRead ?? 0) + (자식.usage.cacheRead ?? 0);
|
|
1442
|
+
session.usage.cacheWrite = (session.usage.cacheWrite ?? 0) + (자식.usage.cacheWrite ?? 0);
|
|
1443
|
+
session.usage.reasoning = (session.usage.reasoning ?? 0) + (자식.usage.reasoning ?? 0);
|
|
1444
|
+
session.usage.calls += 자식.usage.calls;
|
|
1445
|
+
session.usage.ms += 자식.usage.ms;
|
|
1446
|
+
session.usage.retries = (session.usage.retries ?? 0) + (자식.usage.retries ?? 0);
|
|
1447
|
+
session.usage.못잰것 = (session.usage.못잰것 ?? 0) + (자식.usage.못잰것 ?? 0);
|
|
1448
|
+
|
|
1449
|
+
const 글 = 하위요약({
|
|
1450
|
+
목적, 모드: 자식모드, 끝, 모델: 모델알림?.말 ?? null,
|
|
1451
|
+
글자수: 요약길이(conn.ctx),
|
|
1452
|
+
보인이름: (경로) => ctx.scope?.show?.(경로) ?? 경로,
|
|
1453
|
+
});
|
|
1454
|
+
session.push(toolMessage(conn.kind, { callId: call.id, name: 'Task', content: 글 }));
|
|
1455
|
+
ctx.audit.tool('Task', { 목적 }, { summary: 글.slice(0, 300) });
|
|
1456
|
+
yield { type: 'task_done', 목적, 모드: 자식모드, 끝, 모델: 모델알림?.말 ?? null };
|
|
1457
|
+
|
|
1458
|
+
// 사용자가 중단했으면 부모도 여기서 멈춘다. 하위만 끊고 이어가면
|
|
1459
|
+
// 무엇이 중단된 것인지 알 수 없는 화면이 된다.
|
|
1460
|
+
if (끝?.type === 'aborted') { yield { type: 'aborted', steps, kept: true }; return; }
|
|
1461
|
+
if (끝?.type !== 'done') {
|
|
1462
|
+
lastToolFailed = true;
|
|
1463
|
+
// 하위가 계속 못 끝내면 부모가 같은 덩이를 또 떼어 준다. 그건 헛도는 것이다.
|
|
1464
|
+
if (막힘셈(call, `하위 ${끝?.type ?? '실패'}`)) {
|
|
1465
|
+
멈출까 = `하위 작업 "${목적}" 이 계속 끝을 못 봅니다`;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// 여럿을 같이 돌릴 때는 '시작' 을 따로 알리지 않는다.
|
|
1472
|
+
// 화면에서 이름 셋이 먼저 뜨고 결과 셋이 뒤에 몰려 붙으면, 어느 결과가
|
|
1473
|
+
// 어느 파일 것인지 읽을 수 없다. 그럴 때는 끝난 것부터 이름과 결과를 함께 그린다.
|
|
1474
|
+
const 함께 = 실행할것.length > 1;
|
|
1475
|
+
if (!함께) yield { type: 'tool_start', name: 실행할것[0].name, args: 실행할것[0].args };
|
|
1476
|
+
else yield { type: 'tools_start', names: 실행할것.map((x) => x.name), count: 실행할것.length };
|
|
1477
|
+
|
|
1478
|
+
const 한개 = async (call) => {
|
|
1479
|
+
const t0 = Date.now();
|
|
1480
|
+
let result;
|
|
1481
|
+
try { result = await runTool(call.name, call.args, ctx); }
|
|
1482
|
+
catch (err) { result = { error: String(err?.message ?? err) }; }
|
|
1483
|
+
return { call, result, ms: Date.now() - t0 };
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
// 여럿이면 동시에. 읽기만 하는 것들이라 서로 방해하지 않는다.
|
|
1487
|
+
const 결과들 = 함께
|
|
1488
|
+
? await Promise.all(실행할것.map(한개))
|
|
1489
|
+
: [await 한개(실행할것[0])];
|
|
1490
|
+
|
|
1491
|
+
for (const { call, result, ms } of 결과들) {
|
|
1492
|
+
session.usage.ms += ms;
|
|
1493
|
+
/*
|
|
1494
|
+
* 이 모델이 실제로 어떻게 하고 있는지 (agent/grade.js).
|
|
1495
|
+
*
|
|
1496
|
+
* Edit 실패는 '파일에 있는 글을 그대로 옮겨 적는' 능력을 그대로 잰다 —
|
|
1497
|
+
* 작은 모델이 제일 자주 걸리는 자리다. 여기서 세어 두면 다음 걸음의
|
|
1498
|
+
* 프롬프트가 "Read 한 바로 다음 걸음에 고쳐라" 로 바뀐다.
|
|
1499
|
+
*/
|
|
1500
|
+
if (call.name === 'Edit' && result.error) session.본것?.본것('편집실패');
|
|
1501
|
+
else if (!result.error) session.본것?.본것('도구성공');
|
|
1502
|
+
|
|
1503
|
+
/*
|
|
1504
|
+
* 겪은 것을 디스크에도 쌓는다 (agent/evolve.js).
|
|
1505
|
+
*
|
|
1506
|
+
* 위의 지켜본것은 이 대화가 끝나면 사라진다. 그래서 어제 알아낸 것을
|
|
1507
|
+
* 오늘 또 알아내야 했다 — `pnpm` 이 이 PC 에 없다는 걸 매일 다시 겪는다.
|
|
1508
|
+
* 명령의 성패는 이 폴더의 사실이므로 폴더 쪽에 남긴다.
|
|
1509
|
+
*/
|
|
1510
|
+
if (call.name === 'Bash' && call.args?.command) {
|
|
1511
|
+
ctx.배움?.명령본것(call.args.command, !result.error, result.error ?? '');
|
|
1512
|
+
}
|
|
1513
|
+
if (call.name === 'Edit' && result.error) ctx.배움?.모델본것(conn.model, '편집실패');
|
|
1514
|
+
|
|
1515
|
+
if (result.error) {
|
|
1516
|
+
lastToolFailed = true;
|
|
1517
|
+
// 같은 도구가 같은 이유로 계속 실패하면 헛돌고 있는 것이다.
|
|
1518
|
+
// 오류의 첫 줄만 본다 — 뒤에 붙는 경로·숫자는 매번 달라도 원인은 같다.
|
|
1519
|
+
const 이유 = String(result.error).split('\n')[0].slice(0, 60);
|
|
1520
|
+
if (막힘셈(call, 이유)) 멈출까 = `${call.name} 이 같은 이유로 계속 실패합니다 — ${이유}`;
|
|
1521
|
+
}
|
|
1522
|
+
if (call.name === 'Read' && result.content) session.noteRead(call.args.file_path, result.content);
|
|
1523
|
+
// 실제로 파일이 바뀐 것만 적는다. 턴 끝에 이 목록을 디스크와 견준다.
|
|
1524
|
+
if (result.changed) 손댄파일.add(result.changed);
|
|
1525
|
+
// 한 번에 여러 개를 쓴 경우. changed 하나만 보면 나머지가 조용히 빠져서,
|
|
1526
|
+
// 턴 끝에 "만들어졌다" 고 확인해 주는 파일이 넷 중 하나만 나온다.
|
|
1527
|
+
for (const f of result.여럿 ?? []) if (f.ok && f.path) 손댄파일.add(f.path);
|
|
1528
|
+
/*
|
|
1529
|
+
* 옮긴 것도 손댄 것이다.
|
|
1530
|
+
*
|
|
1531
|
+
* Move 는 파일을 고치는 게 아니라 자리를 바꾸므로 changed·여럿 이
|
|
1532
|
+
* 아니라 바뀐것들 로 온다. 이걸 안 세면 스물두 개를 옮겨 놓고도
|
|
1533
|
+
* 이 턴은 "아무것도 안 했다" 고 알고 있게 된다.
|
|
1534
|
+
*/
|
|
1535
|
+
for (const f of result.바뀐것들 ?? []) 손댄파일.add(f);
|
|
1536
|
+
|
|
1537
|
+
// 앞에서 똑같이 부른 적이 있고 결과도 같으면, 결과를 다시 싣지 않는다.
|
|
1538
|
+
let 실을것 = 실을글(result);
|
|
1539
|
+
const 날것 = 실을것; // 되풀이는 도구가 진짜 돌려준 것으로 센다(아래 참고)
|
|
1540
|
+
/*
|
|
1541
|
+
* ── 읽은 파일은 **바뀐 만큼만** 싣는다 ────────────────────────────
|
|
1542
|
+
*
|
|
1543
|
+
* 아래 되풀이 막기는 「인자도 결과도 똑같을 때」만 걸리고, 그 표는
|
|
1544
|
+
* 이 턴 하나짜리다. 그래서 제일 흔한 모양이 그물을 빠져나갔다 —
|
|
1545
|
+
* 읽고, 한 줄 고치고, 확인하러 다시 읽는 것. 결과가 달라졌으니
|
|
1546
|
+
* 1,200줄이 통째로 한 벌 더 실렸다. 새로운 것은 한 줄뿐인데.
|
|
1547
|
+
*
|
|
1548
|
+
* 여기서는 앞서 실은 글과 견줘서, 그대로면 그렇다고만 적고 달라졌으면
|
|
1549
|
+
* 바뀐 자리만 적는다. 창이 밀리지 않으니 앞선 대화가 그만큼 오래 산다.
|
|
1550
|
+
*/
|
|
1551
|
+
/*
|
|
1552
|
+
* ── 할 일 목록은 세션에도 들고 있는다 ────────────────────────────
|
|
1553
|
+
*
|
|
1554
|
+
* 여태 목록이 사는 자리는 도구 결과 하나뿐이었다. 그런데 도구 결과는
|
|
1555
|
+
* 55%에서 한 줄로 접히고 80%에서 요약에 뭉개진다. 접힘 문구는
|
|
1556
|
+
* 「필요하면 다시 읽으세요」인데 **할 일 목록은 다시 읽을 파일이 없다.**
|
|
1557
|
+
* 그래서 긴 작업일수록 남은 항목이 통째로 사라졌다 — 「대화를 잘
|
|
1558
|
+
* 기억 못 한다」로 제보받은 것의 큰 몫이 여기다.
|
|
1559
|
+
*
|
|
1560
|
+
* ctx.todos 는 우리 코드만 본다. 모델이 보는 자리에 남기려면 접거나
|
|
1561
|
+
* 줄일 때 다시 실어 줘야 하고, 그러려면 session 이 들고 있어야 한다.
|
|
1562
|
+
*/
|
|
1563
|
+
if (!result.error && Array.isArray(result.todos)) session.할일 = result.todos;
|
|
1564
|
+
|
|
1565
|
+
if (!result.error && result.읽은것 && session.파일기억) {
|
|
1566
|
+
const 정한것 = session.파일기억.실을것(result.읽은것, 실을것, { 부분인가: !!result.부분인가 });
|
|
1567
|
+
if (정한것.어떻게 !== 'full') {
|
|
1568
|
+
session.본것?.본것('아낀읽기');
|
|
1569
|
+
실을것 = 정한것.글;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (!result.error) {
|
|
1573
|
+
const 서명 = 서명만들기(call);
|
|
1574
|
+
/*
|
|
1575
|
+
* 결과도 한 모양으로 맞춰 견준다. 여러 도구가 받은 경로를 답에 그대로
|
|
1576
|
+
* 되돌려 쓰므로, 자모가 쪼개진 경로로 부르면 결과까지 달라 보인다.
|
|
1577
|
+
*
|
|
1578
|
+
* 견주는 것은 **바꿔치기 전** 글이다. 위에서 파일 기억이 실을것 을
|
|
1579
|
+
* 「앞에서 읽은 그대로입니다」 로 갈아 끼우는데, 그것까지 표에 넣으면
|
|
1580
|
+
* 같은 파일을 두 번째 읽을 때가 첫 번째와 달라 보인다. 그러면 되풀이가
|
|
1581
|
+
* 한 바퀴 늦게 걸린다 — 헛도는 것을 한 걸음 더 돌고 나서야 잡는 셈이다.
|
|
1582
|
+
* 도구가 실제로 무엇을 돌려줬나로 세는 것이 맞다.
|
|
1583
|
+
*/
|
|
1584
|
+
const 같은꼴 = 날것.normalize('NFC');
|
|
1585
|
+
const 앞것 = 부른것.get(서명);
|
|
1586
|
+
if (앞것 !== undefined && 앞것 === 같은꼴) {
|
|
1587
|
+
session.본것?.본것('되풀이');
|
|
1588
|
+
/*
|
|
1589
|
+
* ── 되풀이와 「일하면서 장부를 다시 쓴 것」 은 다르다 ──────────
|
|
1590
|
+
*
|
|
1591
|
+
* 「폴더 정리 해줘」 에서 이 자리가 사람의 턴을 죽였다. 화면은
|
|
1592
|
+
* 이랬다.
|
|
1593
|
+
*
|
|
1594
|
+
* Move 7개 옮김 → TodoWrite → Move 22개 옮김 → TodoWrite
|
|
1595
|
+
* → ⊘ 같은 자리에서 헛돌고 있어 멈췄습니다
|
|
1596
|
+
*
|
|
1597
|
+
* 파일은 스물아홉 개가 실제로 움직이고 있었다. 헛돈 것은 할 일
|
|
1598
|
+
* 목록을 같은 값으로 다시 쓴 것뿐이다. 그런데 세는 자리는 그
|
|
1599
|
+
* 사이에 무슨 일이 있었는지를 안 봤다 — 같은 부름이 세 번이면
|
|
1600
|
+
* 그것으로 끝이었다.
|
|
1601
|
+
*
|
|
1602
|
+
* 이 파일 머리말에 스스로 적어 둔 원칙이 있다: 헛도는 것은
|
|
1603
|
+
* '잘 되고 있는 긴 작업' 과 구분해야 한다. 그 구분을 여기에
|
|
1604
|
+
* 넣는다 — **그 사이에 파일이 진짜로 바뀌었으면 처음부터 다시
|
|
1605
|
+
* 센다.** 아무것도 안 바뀐 채로 되풀이하는 것만 헛도는 것이다.
|
|
1606
|
+
*
|
|
1607
|
+
* 이러면 끝없이 돌 수 있지 않나: 아니다. 손댄파일 은 서로 다른
|
|
1608
|
+
* 파일만 세므로, 같은 자리를 맴돌면 안 늘어난다. 늘어난다는 것은
|
|
1609
|
+
* 매번 새 파일이 움직였다는 뜻이고, 그건 일이 되고 있는 것이다.
|
|
1610
|
+
* 그래도 걸음 수 상한이 마지막 울타리로 남아 있다.
|
|
1611
|
+
*/
|
|
1612
|
+
const 그새한일 = 손댄파일.size > (반복진척.get(서명) ?? 0);
|
|
1613
|
+
const n = 그새한일 ? 1 : (막힘.get(`반복|${서명}`) ?? 0) + 1;
|
|
1614
|
+
막힘.set(`반복|${서명}`, n);
|
|
1615
|
+
반복진척.set(서명, 손댄파일.size);
|
|
1616
|
+
실을것 = `앞에서 부른 ${call.name} 과 인자도 결과도 같습니다. 결과는 그대로라 다시 싣지 않습니다.`
|
|
1617
|
+
+ ' 같은 것을 또 부르지 말고 다음 일로 넘어가세요.';
|
|
1618
|
+
if (n + 1 >= MAX_SAME) 멈출까 = `${call.name} 을 같은 인자로 계속 부르고 있습니다 — 결과가 매번 같습니다`;
|
|
1619
|
+
} else {
|
|
1620
|
+
부른것.set(서명, 같은꼴);
|
|
1621
|
+
반복진척.set(서명, 손댄파일.size);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/*
|
|
1626
|
+
* ── 비밀 가리기 ─────────────────────────────────────────────────
|
|
1627
|
+
*
|
|
1628
|
+
* 도구 결과가 **가려진 채로** 대화에 들어가는 자리는 여기다. 여기서
|
|
1629
|
+
* 막으면 모델에게도 안 가고 `.deel/sessions/*.jsonl` 에도 안 적힌다.
|
|
1630
|
+
* 한 번 새면 두 벌이 되는 것을 한 곳에서 끊는다.
|
|
1631
|
+
*
|
|
1632
|
+
* 다만 `toolMessage` 를 미는 자리가 여기만은 아니다 — 하위 작업 요약과
|
|
1633
|
+
* 살린쓰기는 딴 길로 들어온다. 하위 쪽은 제 루프가 이 자리를 이미
|
|
1634
|
+
* 지나온 글을 넘겨받는 것이라 성립하지만, **새 길을 낼 때는 여기를
|
|
1635
|
+
* 같이 봐야 한다.** 「유일한 자리」 라고 적어 두면 그때 안 본다.
|
|
1636
|
+
*
|
|
1637
|
+
* 새는 자리는 거의 항상 명령 출력이다 — env · git remote -v · curl -v ·
|
|
1638
|
+
* 검사 실패 로그. 그래서 그런 도구만 가린다.
|
|
1639
|
+
*
|
|
1640
|
+
* 파일에서 읽어 온 글은 **안 가린다.** 가리면 모델이 가려진 글을 보고
|
|
1641
|
+
* 그대로 되돌려 써서, 진짜 열쇠가 있던 자리에 표가 적힌다 — 비밀을
|
|
1642
|
+
* 지키려다 비밀을 지우는 셈이다. 대신 무엇이 들어왔는지 알린다.
|
|
1643
|
+
*/
|
|
1644
|
+
/*
|
|
1645
|
+
* 가릴 값도 **환경에 있는 열쇠를 전부** 받는다.
|
|
1646
|
+
*
|
|
1647
|
+
* 여기가 `[conn.key, process.env.DEEL_API_KEY]` 였다. 지금 연결에 안
|
|
1648
|
+
* 쓰는 프로필 열쇠(`DEEL_KEY_PROD`)는 이 목록에 없어서, 자식이 뱉은
|
|
1649
|
+
* 글에 그 값이 그대로 있어도 **정확값 가리기가 못 잡았다.** 이름
|
|
1650
|
+
* 정규식(safety/secrets.js)도 `_PROD` 꼬리 때문에 못 잡는다.
|
|
1651
|
+
* 무엇이 열쇠인지는 config.js 한 곳만 안다 (환경속열쇠들).
|
|
1652
|
+
*/
|
|
1653
|
+
const 아는열쇠들 = [conn.key, ...환경속열쇠들()].filter(Boolean);
|
|
1654
|
+
let 비밀 = [];
|
|
1655
|
+
// 바깥으로 나가면 파일에서 읽어 온 글까지 가린다 (safety/secrets.js 의 가릴까).
|
|
1656
|
+
const 이번엔가릴까 = 가릴까(call.name, { 바깥: 바깥으로나감 });
|
|
1657
|
+
if (이번엔가릴까) {
|
|
1658
|
+
const 가린 = 가리기(실을것, { 열쇠들: 아는열쇠들 });
|
|
1659
|
+
if (가린.가린것.length) {
|
|
1660
|
+
비밀 = 가린.가린것;
|
|
1661
|
+
실을것 = 가린.글 + 가렸다는말(가린.가린것);
|
|
1662
|
+
}
|
|
1663
|
+
} else if (result.content) {
|
|
1664
|
+
// 안 고치고 보기만 한다. 사람이 알아야 손을 쓸 수 있다.
|
|
1665
|
+
비밀 = 훑기(실을것, { 열쇠들: 아는열쇠들 });
|
|
1666
|
+
}
|
|
1667
|
+
if (비밀.length) {
|
|
1668
|
+
ctx.audit?.write?.('secret', {
|
|
1669
|
+
tool: call.name,
|
|
1670
|
+
가렸나: 이번엔가릴까,
|
|
1671
|
+
바깥: 바깥으로나감,
|
|
1672
|
+
무엇: 비밀.map((x) => `${x.종류}×${x.몇번}`).join(' '),
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
session.push(toolMessage(conn.kind, {
|
|
1677
|
+
callId: call.id,
|
|
1678
|
+
name: call.name,
|
|
1679
|
+
content: 실을것,
|
|
1680
|
+
}));
|
|
1681
|
+
if (result.그림) 붙일그림.push(result.그림);
|
|
1682
|
+
yield {
|
|
1683
|
+
type: 'tool',
|
|
1684
|
+
name: call.name,
|
|
1685
|
+
args: call.args,
|
|
1686
|
+
result,
|
|
1687
|
+
ms,
|
|
1688
|
+
parallel: 함께,
|
|
1689
|
+
...(비밀.length ? { 비밀: { 가렸나: 이번엔가릴까, 말: 봤다는말(비밀) } } : {}),
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// 연 그림들을 사람 말 자리로 붙인다 (backend/vision.js).
|
|
1695
|
+
if (붙일그림.length) {
|
|
1696
|
+
const 이름들 = 붙일그림.map((g) => g.show).join(' · ');
|
|
1697
|
+
session.push(그림메시지(conn.kind, {
|
|
1698
|
+
글: 붙일그림.length === 1
|
|
1699
|
+
? `방금 연 그림입니다: ${이름들}`
|
|
1700
|
+
: `방금 연 그림 ${붙일그림.length}장입니다: ${이름들}`,
|
|
1701
|
+
그림들: 붙일그림,
|
|
1702
|
+
}));
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// 도구를 돌리다 끕어졌어도 모델이 한 말은 이미 대화에 들어가 있다. 그건 남는다.
|
|
1706
|
+
if (signal?.aborted) { yield { type: 'aborted', steps, kept: true }; return; }
|
|
1707
|
+
|
|
1708
|
+
// 헛돌고 있으면 여기서 끊는다. 걸음 수가 남았다고 계속 두면
|
|
1709
|
+
// 컨텍스트만 채우고 사람은 기다리기만 한다.
|
|
1710
|
+
if (멈출까) {
|
|
1711
|
+
ctx.audit.blocked('같은 자리를 반복해 스스로 멈춤', 멈출까);
|
|
1712
|
+
yield { type: 'stuck', why: 멈출까, steps, files: 마무리(), 빠진: 빠뜨린것(마지막말()) };
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/*
|
|
1719
|
+
* 걸음을 다 썼다.
|
|
1720
|
+
*
|
|
1721
|
+
* 전에는 `도구 호출 24회에서 멈췄습니다` 한 줄이 전부였다. 무엇이 끝났고
|
|
1722
|
+
* 무엇이 안 끝났는지는 사람이 위로 스크롤해 도구 줄을 세어 봐야 알았다.
|
|
1723
|
+
* 할 일 목록을 이미 들고 있으니 그걸 그대로 보여 준다 — "이어서 해줘" 라고
|
|
1724
|
+
* 할 때 무엇을 이어야 하는지가 화면에 있어야 한다.
|
|
1725
|
+
*/
|
|
1726
|
+
const 남은할일 = (ctx.todos ?? []).filter((x) => x.state !== 'done');
|
|
1727
|
+
yield { type: 'limit', steps, files: 마무리(), 남은할일, 빠진: 빠뜨린것(마지막말()) };
|
|
1728
|
+
}
|