@ryuu-reinzz/ryuu-cli 0.4.0 → 0.4.2
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/CHANGELOG.md +17 -0
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/agent/agent.js +72 -0
- package/src/cli/renderer.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
Semua perubahan penting pada Ryuu Agent CLI didokumentasikan di sini.
|
|
4
4
|
Project ini mengikuti [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.4.2] - 2026-10-31
|
|
7
|
+
|
|
8
|
+
### Diperbaiki
|
|
9
|
+
|
|
10
|
+
- **Respons terpotong di batas token output tidak lagi menyelesaikan tugas
|
|
11
|
+
palsu.** Sebelumnya respons dengan `finish_reason: "length"` yang tidak
|
|
12
|
+
membawa tool call utuh (panggilannya dibuang provider karena JSON-nya
|
|
13
|
+
terpotong) dianggap jawaban akhir, sehingga run berhenti dengan status
|
|
14
|
+
`completed` padahal model masih di tengah pekerjaan dan `changed: none` —
|
|
15
|
+
tugas selesai tanpa satu pun perubahan. Kini potongan itu tetap masuk
|
|
16
|
+
riwayat, model diberi nudge yang berputar (maksimum 3 kali, tampil sebagai
|
|
17
|
+
heartbeat `TRUNCATED_RESPONSE`) untuk mengirim panggilan yang lebih kecil,
|
|
18
|
+
dan bila tetap terpotong tugas dihentikan dengan status baru `truncated`
|
|
19
|
+
(exit code 1, baris merah `✕`) beserta issue yang menjelaskan penyebabnya.
|
|
20
|
+
Respons kosong karena terpotong ikut jalur ini, bukan lagi retry tak
|
|
21
|
+
berujung.
|
|
22
|
+
|
|
6
23
|
## [0.3.2] - 2026-10-30
|
|
7
24
|
|
|
8
25
|
### Ditambahkan
|
package/README.md
CHANGED
|
@@ -782,7 +782,13 @@ berpura-pura sebaliknya.
|
|
|
782
782
|
sudah diketahui.
|
|
783
783
|
- `finish_reason: \"length\"` ditangani: pemanggilan tool yang terpotong pada
|
|
784
784
|
batas token output dibuang dan dilaporkan, tidak pernah dieksekusi sebagai
|
|
785
|
-
file yang tertulis setengah jalan.
|
|
785
|
+
file yang tertulis setengah jalan. Respons yang terpotong **tanpa** tool call
|
|
786
|
+
yang utuh juga tidak pernah dianggap jawaban akhir: model diminta mengulang
|
|
787
|
+
dengan panggilan yang lebih kecil (maksimum 3 kali, terlihat sebagai
|
|
788
|
+
heartbeat `TRUNCATED_RESPONSE`), dan bila tetap terpotong tugas dihentikan
|
|
789
|
+
dengan status `truncated` — bukan `completed` yang palsu. Sisa teks yang
|
|
790
|
+
terpotong tetap masuk riwayat percakapan supaya model melanjutkan, bukan
|
|
791
|
+
mengulang analisis.
|
|
786
792
|
- Pada 70% anggaran waktu, model diberi tahu untuk menyatu (konvergen), dan
|
|
787
793
|
aliran respons yang tidak mengirim byte selama
|
|
788
794
|
`agent.streamIdleTimeoutMs` dibatalkan dengan `STREAM_STALLED` alih-alih
|
package/package.json
CHANGED
package/src/agent/agent.js
CHANGED
|
@@ -68,10 +68,36 @@ function emptyResponseNudge(attempt) {
|
|
|
68
68
|
'The response was empty again. Reply with a tool call or the final answer — never an empty turn, and do not repeat work already done.',
|
|
69
69
|
'Still empty. Do not re-analyze or re-read anything the worklog already covers: if the task is finished, write the result now; otherwise call the next tool.',
|
|
70
70
|
'Empty response repeated. State the next concrete action and call the tool for it, or deliver the final answer below.',
|
|
71
|
+
]; return nudges[Math.min(Math.max(0, attempt - 1), nudges.length - 1)];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* How many cut-off responses are re-asked before the task is stopped.
|
|
76
|
+
*
|
|
77
|
+
* Unlike an empty response, a truncated one is not transient: the model is
|
|
78
|
+
* producing more than `maxOutputTokens` allows, so the retry has to be bounded
|
|
79
|
+
* or the loop would only end at the task deadline.
|
|
80
|
+
*/
|
|
81
|
+
const MAX_TRUNCATED_RESPONSES = 3;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wording that gets a response cut off at the output-token cap going again.
|
|
85
|
+
*
|
|
86
|
+
* The tool call it was writing came back unparseable, so the provider dropped
|
|
87
|
+
* it and nothing ran — the model has to be told that explicitly, or it assumes
|
|
88
|
+
* its work already happened. Rotated like the empty-response nudges.
|
|
89
|
+
*/
|
|
90
|
+
function truncatedResponseNudge(attempt) {
|
|
91
|
+
const nudges = [
|
|
92
|
+
'Your previous response was cut off at the output-token limit before its tool call finished, so NOTHING was executed. Continue the task from where you stopped: send one smaller tool call (narrower file range, shorter content, or split the change into several edits). Do not restart the analysis and do not re-read what you already read.',
|
|
93
|
+
'That response hit the output-token cap again, so the call was discarded and nothing ran. Skip the preamble and act in pieces: one small tool call per step instead of one large one.',
|
|
94
|
+
'Cut off at the token cap once more. No work was lost, but nothing after the cut was executed: issue a single minimal tool call now, or give the final answer without repeating what is already done.',
|
|
95
|
+
'The response keeps hitting the output-token limit. Emit the smallest possible next action immediately — one short tool call or the final answer, no explanation first.',
|
|
71
96
|
];
|
|
72
97
|
return nudges[Math.min(Math.max(0, attempt - 1), nudges.length - 1)];
|
|
73
98
|
}
|
|
74
99
|
|
|
100
|
+
|
|
75
101
|
/**
|
|
76
102
|
* Replace invalid `function.arguments` in a message history with valid JSON.
|
|
77
103
|
*
|
|
@@ -690,6 +716,7 @@ export class Agent {
|
|
|
690
716
|
const failureCounts = new Map();
|
|
691
717
|
try {
|
|
692
718
|
let emptyResponses = 0;
|
|
719
|
+
let truncatedResponses = 0;
|
|
693
720
|
let finished = false;
|
|
694
721
|
for (let step = 1; step <= stepLimit; step += 1) {
|
|
695
722
|
checkAbort();
|
|
@@ -797,6 +824,50 @@ export class Agent {
|
|
|
797
824
|
|
|
798
825
|
if (calls.length === 0) {
|
|
799
826
|
const content = (response.content || '').trim();
|
|
827
|
+
|
|
828
|
+
// A response cut off at the token cap is never a final answer: the
|
|
829
|
+
// model was still mid-task, and the tool call it was writing came
|
|
830
|
+
// back unparseable, so the provider dropped it. Accepting the
|
|
831
|
+
// leftover text as the answer reported the task as "completed" with
|
|
832
|
+
// nothing changed — the worst possible outcome for a long run.
|
|
833
|
+
if (truncated) {
|
|
834
|
+
truncatedResponses += 1;
|
|
835
|
+
if (truncatedResponses <= MAX_TRUNCATED_RESPONSES) {
|
|
836
|
+
if (response.content) messages.push({ role: 'assistant', content: response.content });
|
|
837
|
+
messages.push({ role: 'user', content: truncatedResponseNudge(truncatedResponses) });
|
|
838
|
+
const retryBase = Math.max(100, Number(this.config.recoveryBackoffMs ?? 500));
|
|
839
|
+
const delayMs = Math.min(15000, retryBase * 2 ** Math.min(truncatedResponses - 1, 5));
|
|
840
|
+
this.#emit('heartbeat', {
|
|
841
|
+
phase: 'retry',
|
|
842
|
+
step,
|
|
843
|
+
attempt: truncatedResponses,
|
|
844
|
+
maxAttempts: MAX_TRUNCATED_RESPONSES,
|
|
845
|
+
delayMs,
|
|
846
|
+
code: 'TRUNCATED_RESPONSE',
|
|
847
|
+
message: 'Cut off at the output-token cap — asking for a smaller call.',
|
|
848
|
+
});
|
|
849
|
+
this.logger?.warn('response truncated at the token cap; retrying', {
|
|
850
|
+
step,
|
|
851
|
+
attempt: truncatedResponses,
|
|
852
|
+
delayMs,
|
|
853
|
+
});
|
|
854
|
+
await abortableDelay(delayMs, signal);
|
|
855
|
+
// Repeating this turn must not eat the step budget either.
|
|
856
|
+
step -= 1;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
// The retry budget is spent: stop honestly instead of claiming a
|
|
860
|
+
// completion that no tool result ever backed.
|
|
861
|
+
status = 'truncated';
|
|
862
|
+
finished = true;
|
|
863
|
+
finalText = `The response was cut off at the output-token limit ${MAX_TRUNCATED_RESPONSES} times in a row, so the task was stopped instead of being reported as complete. Nothing further was executed. Raise agent.maxOutputTokens (this run used ${this.config.maxOutputTokens}) or split the task into a smaller one, then continue.`;
|
|
864
|
+
session.issues.push(
|
|
865
|
+
`Stopped: the model hit the ${this.config.maxOutputTokens}-output-token cap before it could finish a tool call.`,
|
|
866
|
+
);
|
|
867
|
+
this.logger?.warn('task stopped after repeated truncation', { step, attempts: truncatedResponses });
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
|
|
800
871
|
if (!content) {
|
|
801
872
|
emptyResponses += 1;
|
|
802
873
|
// An empty answer is a retry, never a fatal fault: nudge the model,
|
|
@@ -828,6 +899,7 @@ export class Agent {
|
|
|
828
899
|
break;
|
|
829
900
|
}
|
|
830
901
|
emptyResponses = 0;
|
|
902
|
+
truncatedResponses = 0;
|
|
831
903
|
if (response.content) {
|
|
832
904
|
// When the answer already streamed live, the text is already on screen.
|
|
833
905
|
const streamed = typeof this.renderer?.streamedResponse === 'function' && this.renderer.streamedResponse();
|
package/src/cli/renderer.js
CHANGED
|
@@ -33,6 +33,7 @@ const STATUS_STYLE = {
|
|
|
33
33
|
cancelled: { label: 'cancelled', color: 'yellow' },
|
|
34
34
|
failed: { label: 'failed', color: 'red' },
|
|
35
35
|
'step-limit': { label: 'step limit reached', color: 'yellow' },
|
|
36
|
+
truncated: { label: 'response truncated', color: 'red' },
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
const ERROR_HINTS = {
|