deel-local-cli 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +686 -28
- package/README.md +571 -31
- package/bin/deel.js +234 -160
- package/package.json +3 -2
- package/src/agent/compact.js +131 -138
- package/src/agent/effort.js +44 -11
- package/src/agent/loop.js +574 -251
- package/src/agent/memory.js +152 -0
- package/src/agent/mention.js +164 -0
- package/src/agent/modes.js +127 -20
- package/src/agent/recall.js +209 -0
- package/src/agent/route.js +156 -0
- package/src/agent/salvage.js +182 -0
- package/src/agent/session.js +155 -11
- package/src/agent/store.js +36 -11
- package/src/backend/adapter.js +266 -183
- package/src/backend/ctxsize.js +246 -0
- package/src/backend/detect.js +10 -1
- package/src/backend/http.js +23 -0
- package/src/backend/learn.js +102 -0
- package/src/backend/mcp.js +304 -0
- package/src/backend/probe.js +15 -14
- package/src/backend/scan.js +97 -3
- package/src/commands.js +825 -50
- package/src/oneshot.js +327 -0
- package/src/repl.js +715 -414
- package/src/report.js +20 -6
- package/src/safety/guard.js +123 -7
- package/src/safety/undo.js +96 -11
- package/src/tools/encoding.js +24 -2
- package/src/tools/fsutil.js +70 -0
- package/src/tools/index.js +471 -27
- package/src/tools/webfetch.js +37 -1
- package/src/ui/ansi.js +32 -2
- package/src/ui/diff.js +255 -0
- package/src/ui/level.js +6 -2
- package/src/ui/prompt.js +13 -1
- package/src/ui/screen.js +169 -0
- package/src/ui/status.js +98 -24
- package/src/ui/tui.js +419 -0
- package/src/version.js +28 -0
package/src/agent/compact.js
CHANGED
|
@@ -1,138 +1,131 @@
|
|
|
1
|
-
// 컨텍스트가 차면 오래된 대화를 '요약해서' 이어 간다.
|
|
2
|
-
//
|
|
3
|
-
// 그냥 잘라내면 방금까지 뭘 하고 있었는지 모델이 잊는다. 파일을 다시 읽고,
|
|
4
|
-
// 이미 고친 곳을 또 고치고, 정해 둔 방침을 어긴다. 그래서 자르는 대신 접는다.
|
|
5
|
-
//
|
|
6
|
-
// 접을 때 지켜야 하는 것 두 가지:
|
|
7
|
-
// 1) 도구 호출과 그 결과는 한 몸이다. 사이를 끊으면 규격 위반이라 서버가 400 을 낸다.
|
|
8
|
-
// 2) 요약도 모델이 만든다. 그 요청이 실패하면 프로그램이 멈추면 안 된다 —
|
|
9
|
-
// 실패하면 옛 방식(그냥 자르기)으로 물러선다.
|
|
10
|
-
import { chat } from '../backend/adapter.js';
|
|
11
|
-
import { estimateTokens } from './session.js';
|
|
12
|
-
|
|
13
|
-
// 몇 %에서 접기 시작할지. 접고 나서 다시 금방 차면 아무 소용이 없으니 넉넉히 비운다.
|
|
14
|
-
export const COMPACT_AT = 0.8;
|
|
15
|
-
export const KEEP_TAIL_RATIO = 0.3; // 최근 대화 중 남길 비율
|
|
16
|
-
export const KEEP_HEAD = 2; // 맨 처음 요청은 언제나 남긴다 (목표가 거기 있다)
|
|
17
|
-
|
|
18
|
-
const 요약지시 = `지금까지의 대화를 다음 형식으로 요약하세요. 이어서 일할 사람이 이것만 보고도 계속할 수 있어야 합니다.
|
|
19
|
-
추측하지 말고 실제로 오간 내용만 쓰세요. 한국어로, 각 항목 3줄 이내로 쓰세요.
|
|
20
|
-
|
|
21
|
-
## 목표
|
|
22
|
-
사용자가 무엇을 시켰는가.
|
|
23
|
-
|
|
24
|
-
## 한 일
|
|
25
|
-
어떤 파일을 어떻게 고쳤는가. 파일 경로를 그대로 적으세요.
|
|
26
|
-
|
|
27
|
-
## 알아낸 것
|
|
28
|
-
코드·환경에 대해 확인된 사실. 다시 조사하지 않아도 되도록.
|
|
29
|
-
|
|
30
|
-
## 정한 것
|
|
31
|
-
어떤 방침·이름·구조로 하기로 했는가. 사용자가 거절한 것도 적으세요.
|
|
32
|
-
|
|
33
|
-
## 남은 일
|
|
34
|
-
아직 안 한 것.`;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** 지금 접어야 하는가. */
|
|
135
|
-
export function shouldCompact(session, at = COMPACT_AT) {
|
|
136
|
-
const b = session.breakdown();
|
|
137
|
-
return b.total > 0 && b.used / b.total >= at;
|
|
138
|
-
}
|
|
1
|
+
// 컨텍스트가 차면 오래된 대화를 '요약해서' 이어 간다.
|
|
2
|
+
//
|
|
3
|
+
// 그냥 잘라내면 방금까지 뭘 하고 있었는지 모델이 잊는다. 파일을 다시 읽고,
|
|
4
|
+
// 이미 고친 곳을 또 고치고, 정해 둔 방침을 어긴다. 그래서 자르는 대신 접는다.
|
|
5
|
+
//
|
|
6
|
+
// 접을 때 지켜야 하는 것 두 가지:
|
|
7
|
+
// 1) 도구 호출과 그 결과는 한 몸이다. 사이를 끊으면 규격 위반이라 서버가 400 을 낸다.
|
|
8
|
+
// 2) 요약도 모델이 만든다. 그 요청이 실패하면 프로그램이 멈추면 안 된다 —
|
|
9
|
+
// 실패하면 옛 방식(그냥 자르기)으로 물러선다.
|
|
10
|
+
import { chat } from '../backend/adapter.js';
|
|
11
|
+
import { estimateTokens, safeCut, safeHead } from './session.js';
|
|
12
|
+
|
|
13
|
+
// 몇 %에서 접기 시작할지. 접고 나서 다시 금방 차면 아무 소용이 없으니 넉넉히 비운다.
|
|
14
|
+
export const COMPACT_AT = 0.8;
|
|
15
|
+
export const KEEP_TAIL_RATIO = 0.3; // 최근 대화 중 남길 비율
|
|
16
|
+
export const KEEP_HEAD = 2; // 맨 처음 요청은 언제나 남긴다 (목표가 거기 있다)
|
|
17
|
+
|
|
18
|
+
const 요약지시 = `지금까지의 대화를 다음 형식으로 요약하세요. 이어서 일할 사람이 이것만 보고도 계속할 수 있어야 합니다.
|
|
19
|
+
추측하지 말고 실제로 오간 내용만 쓰세요. 한국어로, 각 항목 3줄 이내로 쓰세요.
|
|
20
|
+
|
|
21
|
+
## 목표
|
|
22
|
+
사용자가 무엇을 시켰는가.
|
|
23
|
+
|
|
24
|
+
## 한 일
|
|
25
|
+
어떤 파일을 어떻게 고쳤는가. 파일 경로를 그대로 적으세요.
|
|
26
|
+
|
|
27
|
+
## 알아낸 것
|
|
28
|
+
코드·환경에 대해 확인된 사실. 다시 조사하지 않아도 되도록.
|
|
29
|
+
|
|
30
|
+
## 정한 것
|
|
31
|
+
어떤 방침·이름·구조로 하기로 했는가. 사용자가 거절한 것도 적으세요.
|
|
32
|
+
|
|
33
|
+
## 남은 일
|
|
34
|
+
아직 안 한 것.`;
|
|
35
|
+
|
|
36
|
+
// 자르는 자리를 찾는 두 함수는 session.js 에 있다. 접기와 그냥 줄이기(trim)가
|
|
37
|
+
// **같은 잣대**를 써야 하기 때문이다 — 한쪽만 짝을 맞추면 물러서는 순간 대화가 망가진다.
|
|
38
|
+
// 쓰던 자리가 있으니 이름은 그대로 내보낸다.
|
|
39
|
+
export { safeCut, safeHead } from './session.js';
|
|
40
|
+
|
|
41
|
+
/** 접을 구간과 남길 구간을 나눈다. */
|
|
42
|
+
export function split(messages, { keepHead = KEEP_HEAD, tailRatio = KEEP_TAIL_RATIO } = {}) {
|
|
43
|
+
const n = messages.length;
|
|
44
|
+
const wantTail = Math.max(4, Math.floor(n * tailRatio));
|
|
45
|
+
const cut = safeCut(messages, n - wantTail);
|
|
46
|
+
const head = safeHead(messages, Math.min(keepHead, cut));
|
|
47
|
+
if (cut <= head) return null; // 접을 것이 없다
|
|
48
|
+
const fold = messages.slice(head, cut);
|
|
49
|
+
if (fold.length < 4) return null;
|
|
50
|
+
return { head: messages.slice(0, head), fold, tail: messages.slice(cut) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 요약에 넣을 글. 도구 결과는 길기만 하고 요약에 도움이 안 되니 앞부분만 준다.
|
|
54
|
+
function transcript(msgs) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const m of msgs) {
|
|
57
|
+
const who = m.role === 'user' ? '사용자' : m.role === 'assistant' ? '나' : '도구결과';
|
|
58
|
+
let body = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
|
|
59
|
+
if (m.role === 'tool') body = body.slice(0, 400);
|
|
60
|
+
const calls = (m.tool_calls ?? []).map((t) => {
|
|
61
|
+
const fn = t.function ?? t;
|
|
62
|
+
const args = typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments ?? {});
|
|
63
|
+
return `${fn.name}(${args.slice(0, 160)})`;
|
|
64
|
+
});
|
|
65
|
+
if (calls.length) out.push(`${who}: [도구] ${calls.join(', ')}`);
|
|
66
|
+
if (body.trim()) out.push(`${who}: ${body.trim()}`);
|
|
67
|
+
}
|
|
68
|
+
return out.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 실제로 접는다.
|
|
73
|
+
* @returns {{ok:boolean, folded:number, before:number, after:number, summary?:string, why?:string}}
|
|
74
|
+
*/
|
|
75
|
+
export async function compact(session, { auto = false, signal = null } = {}) {
|
|
76
|
+
const before = session.breakdown().used;
|
|
77
|
+
const parts = split(session.messages);
|
|
78
|
+
if (!parts) return { ok: false, why: '접을 만큼 쌓이지 않았습니다', folded: 0, before, after: before };
|
|
79
|
+
|
|
80
|
+
const text = transcript(parts.fold);
|
|
81
|
+
let summary = null;
|
|
82
|
+
try {
|
|
83
|
+
const r = await chat(session.conn, {
|
|
84
|
+
messages: [
|
|
85
|
+
{ role: 'system', content: '너는 대화를 요약한다. 요약 외에 다른 말을 하지 않는다.' },
|
|
86
|
+
{ role: 'user', content: `${요약지시}\n\n----- 대화 -----\n${text}` },
|
|
87
|
+
],
|
|
88
|
+
maxTokens: 1200,
|
|
89
|
+
think: session.conn.kind === 'ollama' ? false : 'low', // 요약에 깊은 사고는 필요 없다
|
|
90
|
+
// 사용자가 Ctrl+C 를 누르면 이것도 멈춰야 한다. 안 받으면 답하는 도중
|
|
91
|
+
// 화면이 멈춰 있고 아무 키도 안 먹는 것처럼 보인다.
|
|
92
|
+
signal,
|
|
93
|
+
// 요약 하나에 5분을 기다릴 이유가 없다. 그만한 서버면 그냥 줄이는 편이 빠르다.
|
|
94
|
+
timeout: 60000,
|
|
95
|
+
});
|
|
96
|
+
summary = (r.content ?? '').trim();
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// 사용자가 끊은 것은 실패가 아니다. 대화를 건드리지 않고 그대로 둔다 —
|
|
99
|
+
// 여기서 물러서기(trim)까지 해 버리면, 끊었는데 대화가 줄어 있게 된다.
|
|
100
|
+
if (err?.name === 'Aborted' || signal?.aborted) {
|
|
101
|
+
return { ok: false, aborted: true, why: '중단했습니다', folded: 0, before, after: before };
|
|
102
|
+
}
|
|
103
|
+
summary = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 요약을 못 받았으면 옛 방식으로 물러선다. 멈추지는 않는다.
|
|
107
|
+
if (!summary) {
|
|
108
|
+
const folded = session.trim();
|
|
109
|
+
const after = session.breakdown().used;
|
|
110
|
+
return { ok: folded > 0, folded, before, after, why: '요약을 받지 못해 그냥 줄였습니다', fallback: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
session.messages = [
|
|
114
|
+
...parts.head,
|
|
115
|
+
{
|
|
116
|
+
role: 'user',
|
|
117
|
+
content: `[앞선 대화 ${parts.fold.length}개를 요약해 접었습니다. 아래가 그 요약입니다.]\n\n${summary}\n\n[요약 끝. 이어서 진행하세요. 파일 내용이 필요하면 다시 읽으세요.]`,
|
|
118
|
+
},
|
|
119
|
+
...parts.tail,
|
|
120
|
+
];
|
|
121
|
+
session.filesRead.clear(); // 접힌 뒤에는 읽어 둔 파일도 기억에서 지운다
|
|
122
|
+
|
|
123
|
+
const after = session.breakdown().used;
|
|
124
|
+
return { ok: true, folded: parts.fold.length, before, after, summary, auto };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 지금 접어야 하는가. */
|
|
128
|
+
export function shouldCompact(session, at = COMPACT_AT) {
|
|
129
|
+
const b = session.breakdown();
|
|
130
|
+
return b.total > 0 && b.used / b.total >= at;
|
|
131
|
+
}
|
package/src/agent/effort.js
CHANGED
|
@@ -21,34 +21,56 @@ export const STAGES = {
|
|
|
21
21
|
// 출력 상한을 숫자로 못 박으면 안 된다. 컨텍스트 4k 짜리 모델에 4096 을 주면
|
|
22
22
|
// 입력 자리가 하나도 안 남고, 200k 짜리 모델에는 턱없이 모자란다.
|
|
23
23
|
// 그래서 '남은 자리의 몇 %' 로 정하고, 아래위로만 울타리를 친다.
|
|
24
|
+
//
|
|
25
|
+
// share 를 왜 넉넉히 잡나 — 여기서 한 번 잘못 알고 있었다:
|
|
26
|
+
// 상한은 '쓸 예산' 이 아니라 '넘지 못하는 선' 이다. 상한을 작게 잡아도 모델이
|
|
27
|
+
// 짧게 답하면 토큰은 그만큼만 나간다. 아끼는 효과가 없다. 반대로 답이 그 선에
|
|
28
|
+
// 닿으면 **말이 중간에서 끊긴다.** 즉 share 를 조이면 아끼는 게 아니라 자르는 것이다.
|
|
29
|
+
//
|
|
30
|
+
// 실제로 그래서 파일이 안 만들어졌다. 이어가기(work) 가 0.15 였는데, 파일을 쓰는
|
|
31
|
+
// 호출은 대개 이 단계에서 나온다. 32k 모델에서 4,600 토큰 — 1,000줄짜리 HTML 은
|
|
32
|
+
// 그 세 배가 필요하다. 매번 같은 자리에서 잘렸다.
|
|
33
|
+
//
|
|
34
|
+
// 다음 턴이 들어갈 자리는 아래 tokensFor 의 room/2 가 이미 지킨다. 그러니 여기서
|
|
35
|
+
// 또 조일 이유가 없다. 단계별 차이는 남겨 두되, 어느 단계든 한 번은 쓸 만큼 준다.
|
|
24
36
|
export const PROFILES = {
|
|
25
37
|
even: {
|
|
26
38
|
name: '균일',
|
|
27
39
|
desc: '모든 단계 같은 강도 — 예측 가능한 대신 느립니다',
|
|
28
40
|
shift: { plan: 0, work: 0, fix: 0 },
|
|
29
|
-
share: { plan: 0.
|
|
41
|
+
share: { plan: 0.40, work: 0.40, fix: 0.40 },
|
|
30
42
|
},
|
|
31
43
|
save: {
|
|
32
44
|
name: '절약',
|
|
33
45
|
desc: '첫 판단만 세게, 이어가기는 얕게 — 대개 이게 낫습니다',
|
|
34
46
|
shift: { plan: 0, work: -1, fix: +1 },
|
|
35
|
-
share: { plan: 0.
|
|
47
|
+
share: { plan: 0.40, work: 0.35, fix: 0.45 },
|
|
36
48
|
},
|
|
37
49
|
deep: {
|
|
38
50
|
name: '깊게',
|
|
39
51
|
desc: '전 단계 한 칸씩 위로 — 어려운 일에만',
|
|
40
52
|
shift: { plan: +1, work: 0, fix: +1 },
|
|
41
|
-
share: { plan: 0.
|
|
53
|
+
share: { plan: 0.50, work: 0.45, fix: 0.50 },
|
|
42
54
|
},
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
// 울타리.
|
|
46
58
|
// 아래: 이보다 작으면 도구 호출 한 개도 못 뱉는다. 생각을 많이 하는 모델은 더 필요하다.
|
|
47
|
-
// 위: 이 이상은 어떤 모델도 한 번에 잘 안 낸다. 실수로 큰 값을 보내 튕기는 것도 막는다.
|
|
48
59
|
export const MIN_CAP = 512;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
/**
|
|
61
|
+
* 출력 상한을 **모를 때** 쓰는 값.
|
|
62
|
+
*
|
|
63
|
+
* 컨텍스트가 128k 라도 '한 번에 낼 수 있는 출력' 은 대개 훨씬 작다. 모델 한계보다
|
|
64
|
+
* 큰 max_tokens 를 보내면 그냥 거부하는 게이트웨이가 있어서, 모르면 이 선에 선다.
|
|
65
|
+
*
|
|
66
|
+
* **아는 값이 있으면 이 값은 비켜선다.** 전에는 그게 안 됐다 —
|
|
67
|
+
* Math.min(cap, max ?? MAX_CAP, MAX_CAP)
|
|
68
|
+
* 세 번째 인자가 무조건 다시 조여서, 사용자가 적어 둔 max 는 낮출 수만 있고
|
|
69
|
+
* 올릴 수는 없었다. /ctx out 200k 를 해도 16,384 였다. 그런데 주석도 README 도
|
|
70
|
+
* /ctx 안내도 셋 다 '올릴 수 있다' 고 말했다. 문서에 적힌 탈출구가 막혀 있었던 것이다.
|
|
71
|
+
*
|
|
72
|
+
* 아는 값은 두 곳에서 온다 — 사용자가 정한 것(/out)과 서버에서 알아낸 것(backend/ctxsize).
|
|
73
|
+
*/
|
|
52
74
|
export const MAX_CAP = 16384;
|
|
53
75
|
|
|
54
76
|
const ALIAS = { 균일: 'even', 절약: 'save', 깊게: 'deep', uniform: 'even', thrifty: 'save' };
|
|
@@ -97,7 +119,8 @@ export function tokensFor(profileKey, stage, { ctx = 0, used = 0, max = null } =
|
|
|
97
119
|
let cap = Math.floor(room * share);
|
|
98
120
|
// 남은 자리의 절반을 넘겨 주지 않는다. 답이 길어져도 다음 턴이 들어갈 자리는 남겨야 한다.
|
|
99
121
|
cap = Math.min(cap, Math.floor(room / 2));
|
|
100
|
-
|
|
122
|
+
// 아는 상한이 있으면 그것을 따른다. 모를 때만 MAX_CAP 에 선다.
|
|
123
|
+
cap = Math.min(cap, max ?? MAX_CAP);
|
|
101
124
|
cap = Math.max(cap, MIN_CAP);
|
|
102
125
|
// 남은 자리 자체가 바닥이면 울타리 아래라도 남은 만큼만 준다.
|
|
103
126
|
return room > 0 ? Math.min(cap, Math.max(MIN_CAP, room)) : MIN_CAP;
|
|
@@ -106,13 +129,23 @@ export function tokensFor(profileKey, stage, { ctx = 0, used = 0, max = null } =
|
|
|
106
129
|
/** 잘렸을 때 풀어 줄 최대 상한 — 남은 자리를 거의 다 내준다. */
|
|
107
130
|
export function fullCap({ ctx = 0, used = 0, max = null } = {}) {
|
|
108
131
|
const room = ctx > 0 ? Math.max(0, ctx - used) : 4096;
|
|
109
|
-
return Math.max(MIN_CAP, Math.min(Math.floor(room * 0.8), max ?? MAX_CAP
|
|
132
|
+
return Math.max(MIN_CAP, Math.min(Math.floor(room * 0.8), max ?? MAX_CAP));
|
|
110
133
|
}
|
|
111
134
|
|
|
112
|
-
|
|
135
|
+
/**
|
|
136
|
+
* 잘린 응답인가. 잘린 채로 넘어가면 도구 호출이 반토막 나서 조용히 실패한다.
|
|
137
|
+
*
|
|
138
|
+
* finish_reason 만 보면 안 된다. 사내 게이트웨이나 중계 서버는 잘라 놓고도
|
|
139
|
+
* 'stop' 이라고 하는 경우가 실제로 있다. 그러면 상한을 올려 다시 부를 기회를
|
|
140
|
+
* 놓친 채 반토막 호출이 그냥 지나간다.
|
|
141
|
+
*
|
|
142
|
+
* 그런데 인자 JSON 이 깨진 것 자체가 잘렸다는 증거다 — 모델은 반쪽짜리 JSON 을
|
|
143
|
+
* 일부러 만들지 않는다. 그래서 말을 안 해 줘도 이걸 보고 안다.
|
|
144
|
+
*/
|
|
113
145
|
export function wasCut(msg) {
|
|
114
146
|
const s = String(msg?.stopped ?? '');
|
|
115
|
-
|
|
147
|
+
if (s === 'length' || s === 'max_tokens' || s === 'MAX_TOKENS') return true;
|
|
148
|
+
return (msg?.toolCalls ?? []).some((t) => t?.argsBroken);
|
|
116
149
|
}
|
|
117
150
|
|
|
118
151
|
// 화면에 보여줄 표. 상한은 지금 붙어 있는 모델 기준으로 계산해서 보여준다 —
|