@pentoshi/clai 3.9.1 → 3.9.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/dist/agent/runner.js +92 -27
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/stream-recovery.d.ts +83 -0
- package/dist/agent/stream-recovery.js +270 -0
- package/dist/agent/stream-recovery.js.map +1 -0
- package/dist/app/controllers/session-controller.d.ts +1 -0
- package/dist/app/controllers/session-controller.js +11 -1
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/tui-v2/app/App.js +4 -1
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +8 -6
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +1 -1
package/dist/agent/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { streamWithProvider, completeWithProvider
|
|
3
|
+
import { streamWithProvider, completeWithProvider } from "../llm/router.js";
|
|
4
|
+
import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, createStreamRecoveryState, resetStreamRecoveryState, } from "./stream-recovery.js";
|
|
4
5
|
import { resolveToolDialect } from "../llm/capabilities.js";
|
|
5
6
|
import { syntheticToolCallId, isTextOnlyModel, fromWireName, } from "../llm/tool-protocol.js";
|
|
6
7
|
import { sanitizeAssistantText } from "../ui/ansi-box.js";
|
|
@@ -102,6 +103,28 @@ export function shouldYieldForResponderBeforeReport(plan, runningJobs, notificat
|
|
|
102
103
|
!notification.readAt &&
|
|
103
104
|
!notification.analyzedAt));
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Cancellable backoff. Resolves after `ms`, or rejects immediately if the
|
|
108
|
+
* signal aborts (double-Esc) so recovery waits never trap a cancelled turn.
|
|
109
|
+
*/
|
|
110
|
+
function delay(ms, signal) {
|
|
111
|
+
if (ms <= 0)
|
|
112
|
+
return Promise.resolve();
|
|
113
|
+
if (signal?.aborted) {
|
|
114
|
+
return Promise.reject(signal.reason ?? new Error("Aborted"));
|
|
115
|
+
}
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const onAbort = () => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
reject(signal?.reason ?? new Error("Aborted"));
|
|
120
|
+
};
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
signal?.removeEventListener("abort", onAbort);
|
|
123
|
+
resolve();
|
|
124
|
+
}, ms);
|
|
125
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
105
128
|
export async function runAgentTurn(prompt, options = {}) {
|
|
106
129
|
const agentMode = options.mode === "plan" || options.mode === "agent" || options.mode === "ask"
|
|
107
130
|
? options.mode
|
|
@@ -696,6 +719,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
696
719
|
// to actually act instead of silently returning an empty answer.
|
|
697
720
|
let emptyVisibleRetries = 0;
|
|
698
721
|
let retryWithoutThinking = false;
|
|
722
|
+
// Robust stream-failure recovery. When a provider stream/complete fails we
|
|
723
|
+
// try working approaches (backoff, compaction, thinking-off, provider
|
|
724
|
+
// fallback) before surrendering the turn — see ./stream-recovery. Both are
|
|
725
|
+
// reset on any successful stream so each failure episode gets a fresh
|
|
726
|
+
// budget and we only give up in the worst case.
|
|
727
|
+
let allowModelFallback = false;
|
|
728
|
+
const recoveryState = createStreamRecoveryState();
|
|
699
729
|
// Track tool calls truncated by the token limit so we can ask the model
|
|
700
730
|
// to retry in smaller pieces instead of leaking broken JSON as an answer.
|
|
701
731
|
let truncatedToolRetries = 0;
|
|
@@ -2254,6 +2284,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2254
2284
|
/** E4: consecutive free-tier stream failures this turn. */
|
|
2255
2285
|
let freeTierConsecutiveFailures = 0;
|
|
2256
2286
|
let freeTierLargeContextWarned = false;
|
|
2287
|
+
// Surface the free-tier "failed N times / switch provider" advisory at most
|
|
2288
|
+
// once per turn — the recovery planner already narrates each retry, so
|
|
2289
|
+
// repeating this on every failure just adds noise.
|
|
2290
|
+
let freeTierAdvisoryShown = false;
|
|
2257
2291
|
const summarizeForCompaction = async (summaryPrompt) => {
|
|
2258
2292
|
const response = await completeWithProvider({
|
|
2259
2293
|
provider,
|
|
@@ -2522,7 +2556,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2522
2556
|
completion = await streamWithProvider({
|
|
2523
2557
|
provider,
|
|
2524
2558
|
model,
|
|
2525
|
-
allowModelFallback
|
|
2559
|
+
allowModelFallback,
|
|
2526
2560
|
messages,
|
|
2527
2561
|
temperature: /minimax-m3/i.test(model) ? 1.0 : 0.2,
|
|
2528
2562
|
maxTokens: stepMaxTokens,
|
|
@@ -2663,36 +2697,67 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2663
2697
|
}
|
|
2664
2698
|
});
|
|
2665
2699
|
freeTierConsecutiveFailures = 0;
|
|
2700
|
+
// Stream succeeded → the failure episode is over. Reset the recovery
|
|
2701
|
+
// budget and the one-shot fallback flag so a later, unrelated failure
|
|
2702
|
+
// starts fresh (and we never give up while making progress).
|
|
2703
|
+
resetStreamRecoveryState(recoveryState);
|
|
2704
|
+
allowModelFallback = false;
|
|
2666
2705
|
}
|
|
2667
2706
|
catch (streamError) {
|
|
2707
|
+
// User cancelled (double-Esc) — never try to recover, just stop.
|
|
2708
|
+
if (options.signal?.aborted)
|
|
2709
|
+
throw streamError;
|
|
2668
2710
|
// E4: track free-tier failures for advisory notices (never blocks).
|
|
2669
2711
|
freeTierConsecutiveFailures += 1;
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
//
|
|
2683
|
-
//
|
|
2684
|
-
//
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2712
|
+
if (!freeTierAdvisoryShown) {
|
|
2713
|
+
for (const notice of freeTierGuardNotices({
|
|
2714
|
+
provider,
|
|
2715
|
+
estimatedInputTokens: contextBreakdown.estimatedTotalTokens,
|
|
2716
|
+
consecutiveFailures: freeTierConsecutiveFailures,
|
|
2717
|
+
})) {
|
|
2718
|
+
if (notice.includes("Large context"))
|
|
2719
|
+
continue; // already shown above
|
|
2720
|
+
writeNotice("warn", notice, chalk.yellow(` ⚠ ${notice}\n`));
|
|
2721
|
+
freeTierAdvisoryShown = true;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
// Robust recovery: a single flaky provider/model (empty admission,
|
|
2725
|
+
// connection glitch, capacity 5xx, rate limit, or an oversized
|
|
2726
|
+
// request) must not kill the turn. Classify the failure and take a
|
|
2727
|
+
// bounded, escalating recovery step — back off, compact, drop
|
|
2728
|
+
// thinking, or let the router fall back to another provider/model.
|
|
2729
|
+
// We only rethrow (stop the turn) in the worst case: every approach
|
|
2730
|
+
// for that failure class is exhausted or the total budget is spent.
|
|
2731
|
+
const failureKind = classifyStreamFailure(streamError);
|
|
2732
|
+
const plan = planStreamRecovery({
|
|
2733
|
+
kind: failureKind,
|
|
2734
|
+
state: recoveryState,
|
|
2735
|
+
});
|
|
2736
|
+
if (plan.action === "give-up") {
|
|
2737
|
+
throw streamError;
|
|
2738
|
+
}
|
|
2739
|
+
recordRecoveryAttempt(recoveryState, failureKind);
|
|
2740
|
+
if (plan.notice) {
|
|
2741
|
+
writeNotice("warn", plan.notice, chalk.yellow(` ⚠ ${plan.notice}\n`));
|
|
2694
2742
|
}
|
|
2695
|
-
|
|
2743
|
+
if (plan.disableThinking)
|
|
2744
|
+
retryWithoutThinking = true;
|
|
2745
|
+
if (plan.allowModelFallback)
|
|
2746
|
+
allowModelFallback = true;
|
|
2747
|
+
if (plan.forceCompact) {
|
|
2748
|
+
await maybeAutoCompact(`stream-recovery:${failureKind}`, true);
|
|
2749
|
+
}
|
|
2750
|
+
if (plan.nudge) {
|
|
2751
|
+
messages.push(recoveryUserMessage(plan.nudge));
|
|
2752
|
+
}
|
|
2753
|
+
if (plan.delayMs > 0) {
|
|
2754
|
+
emit({
|
|
2755
|
+
type: "status",
|
|
2756
|
+
text: `retrying in ${Math.ceil(plan.delayMs / 1000)}s (${failureKind})`,
|
|
2757
|
+
});
|
|
2758
|
+
await delay(plan.delayMs, options.signal);
|
|
2759
|
+
}
|
|
2760
|
+
continue;
|
|
2696
2761
|
}
|
|
2697
2762
|
}
|
|
2698
2763
|
finally {
|