@pentoshi/clai 3.11.2 → 3.11.4
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 +122 -29
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/stream-recovery.d.ts +15 -1
- package/dist/agent/stream-recovery.js +42 -2
- package/dist/agent/stream-recovery.js.map +1 -1
- package/dist/llm/agentrouter.js +2 -2
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/bynara.js +2 -2
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/http.d.ts +29 -3
- package/dist/llm/http.js +110 -25
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/modal.js +7 -2
- package/dist/llm/modal.js.map +1 -1
- package/dist/llm/nvidia.js +6 -6
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/router.d.ts +1 -1
- package/dist/llm/router.js +59 -15
- package/dist/llm/router.js.map +1 -1
- package/dist/modes/ask.js +91 -23
- package/dist/modes/ask.js.map +1 -1
- package/dist/prompts/embedded.js +2 -2
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/index.js +44 -4
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/system.agent.md +2 -1
- package/dist/prompts/system.ask.md +1 -0
- package/dist/safety/classifier.js +3 -0
- package/dist/safety/classifier.js.map +1 -1
- package/dist/tools/definitions.js +15 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/image.d.ts +19 -1
- package/dist/tools/image.js +149 -1
- package/dist/tools/image.js.map +1 -1
- package/dist/tools/registry.js +13 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/tool-types.d.ts +8 -1
- package/dist/tui-v2/components/transcript/thinking-block.js +6 -7
- package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-view.js +155 -28
- package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -1
- package/dist/tui-v2/components/transcript/use-transcript-selection.d.ts +2 -2
- package/dist/tui-v2/components/transcript/use-transcript-selection.js +7 -8
- package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -1
- package/dist/tui-v2/state/transcript-reducer.js +7 -12
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
- package/dist/types.d.ts +17 -0
- 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
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { completeWithProvider, streamWithProvider } from "../llm/router.js";
|
|
4
4
|
import { streamAlreadyEmitted } from "../llm/stream-progress.js";
|
|
5
5
|
import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, createStreamRecoveryState, resetStreamRecoveryState, } from "./stream-recovery.js";
|
|
6
|
-
import { resolveToolDialect } from "../llm/capabilities.js";
|
|
6
|
+
import { modelSupportsVision, resolveToolDialect } from "../llm/capabilities.js";
|
|
7
7
|
import { syntheticToolCallId, isTextOnlyModel, markTextOnlyModel, fromWireName, } from "../llm/tool-protocol.js";
|
|
8
8
|
import { sanitizeAssistantText } from "../ui/ansi-box.js";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
@@ -168,6 +168,37 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
168
168
|
// the normal tool path already surfaced, which would render it twice. Reset
|
|
169
169
|
// at the top of every loop iteration.
|
|
170
170
|
let visibleCommitted = false;
|
|
171
|
+
let interruptedVisible = "";
|
|
172
|
+
const trimExactContinuationOverlap = (previous, current, minLength = 32) => {
|
|
173
|
+
if (previous.length > 0 && current.startsWith(previous)) {
|
|
174
|
+
return current.slice(previous.length);
|
|
175
|
+
}
|
|
176
|
+
const maxLength = Math.min(previous.length, current.length);
|
|
177
|
+
if (maxLength < minLength)
|
|
178
|
+
return current;
|
|
179
|
+
const pattern = current.slice(0, maxLength);
|
|
180
|
+
const fallback = new Uint32Array(maxLength);
|
|
181
|
+
for (let index = 1, matched = 0; index < maxLength; index += 1) {
|
|
182
|
+
while (matched > 0 && pattern[index] !== pattern[matched]) {
|
|
183
|
+
matched = fallback[matched - 1];
|
|
184
|
+
}
|
|
185
|
+
if (pattern[index] === pattern[matched])
|
|
186
|
+
matched += 1;
|
|
187
|
+
fallback[index] = matched;
|
|
188
|
+
}
|
|
189
|
+
let matched = 0;
|
|
190
|
+
for (let index = previous.length - maxLength; index < previous.length; index += 1) {
|
|
191
|
+
while (matched > 0 && previous[index] !== pattern[matched]) {
|
|
192
|
+
matched = fallback[matched - 1];
|
|
193
|
+
}
|
|
194
|
+
if (previous[index] === pattern[matched])
|
|
195
|
+
matched += 1;
|
|
196
|
+
if (matched === maxLength && index < previous.length - 1) {
|
|
197
|
+
matched = fallback[matched - 1];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return matched >= minLength ? current.slice(matched) : current;
|
|
201
|
+
};
|
|
171
202
|
const noopSpinner = {
|
|
172
203
|
setLabel: () => { },
|
|
173
204
|
bumpReasoning: () => { },
|
|
@@ -331,7 +362,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
331
362
|
jobManager.releaseResponderNotificationClaim(notificationId);
|
|
332
363
|
}
|
|
333
364
|
};
|
|
334
|
-
const finishTurn = (answer, steps, status = "succeeded", remainingCriteria = [], reason) => {
|
|
365
|
+
const finishTurn = (answer, steps, status = "succeeded", remainingCriteria = [], reason, displayAnswer) => {
|
|
335
366
|
releaseUnreadResponderClaims();
|
|
336
367
|
const outcome = createTurnOutcome(normalizeTurnOutcomeInput({
|
|
337
368
|
status,
|
|
@@ -340,13 +371,22 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
340
371
|
remainingCriteria,
|
|
341
372
|
reason,
|
|
342
373
|
}));
|
|
343
|
-
const
|
|
374
|
+
const renderOptions = {
|
|
344
375
|
diagnostics: !suppressOutcomeDiagnostics,
|
|
345
|
-
}
|
|
346
|
-
|
|
376
|
+
};
|
|
377
|
+
const rendered = renderTurnOutcome(outcome, renderOptions);
|
|
378
|
+
const displayRendered = displayAnswer === undefined
|
|
379
|
+
? rendered
|
|
380
|
+
: renderTurnOutcome({ ...outcome, answer: displayAnswer }, renderOptions);
|
|
381
|
+
if (displayRendered.trim()) {
|
|
382
|
+
writeAssistantMessage(displayRendered);
|
|
383
|
+
}
|
|
384
|
+
else if (!writesDirectly) {
|
|
385
|
+
emit({ type: "assistant-message", text: "" });
|
|
386
|
+
}
|
|
347
387
|
if (options.onMessages) {
|
|
348
388
|
try {
|
|
349
|
-
options.onMessages(buildTurnHistory(liveMessages,
|
|
389
|
+
options.onMessages(buildTurnHistory(liveMessages, displayRendered));
|
|
350
390
|
}
|
|
351
391
|
catch {
|
|
352
392
|
// Persisting history must never break the turn.
|
|
@@ -370,10 +410,20 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
370
410
|
const projectContext = await loadProjectContext();
|
|
371
411
|
const hasAttachedImages = Boolean(options.images?.length);
|
|
372
412
|
const imageOcrEnabled = shouldEnableImageOcr(prompt, hasAttachedImages, options.visionProven !== false);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
|
|
413
|
+
const initialProvider = options.provider ?? config.defaultProvider;
|
|
414
|
+
const initialModel = options.model ?? config.defaultModel;
|
|
415
|
+
// image.view is different from optimistic user-attachment handling: once
|
|
416
|
+
// the tool succeeds, the model must actually receive and inspect its bytes.
|
|
417
|
+
// Offer it only with affirmative capability evidence for the active route.
|
|
418
|
+
const routeToolNames = (routeProvider, routeModel) => availableToolNames().filter((name) => {
|
|
419
|
+
if (name === "image.ocr")
|
|
420
|
+
return imageOcrEnabled;
|
|
421
|
+
if (name === "image.view") {
|
|
422
|
+
return modelSupportsVision(routeProvider, routeModel);
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
425
|
+
});
|
|
426
|
+
const toolNames = routeToolNames(initialProvider, initialModel);
|
|
377
427
|
// Build / scaffold / continuation turns must NEVER be diverted into a
|
|
378
428
|
// web.search for "current info". The /implement directive ("Execute it
|
|
379
429
|
// now…") and prompts like "create a react app" contain words such as
|
|
@@ -402,9 +452,9 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
402
452
|
!idleOrSocialPrompt &&
|
|
403
453
|
toolNames.includes("web.search") &&
|
|
404
454
|
requiresFreshWebSearch(prompt);
|
|
405
|
-
let provider =
|
|
455
|
+
let provider = initialProvider;
|
|
406
456
|
await ensureProviderConfigured(provider);
|
|
407
|
-
let model =
|
|
457
|
+
let model = initialModel;
|
|
408
458
|
// Some Groq free-tier models have a per-request/per-minute input budget
|
|
409
459
|
// below the normal agent prompt alone. Select a purpose-built compact
|
|
410
460
|
// instruction set before the request is made, rather than treating the
|
|
@@ -416,13 +466,16 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
416
466
|
return { dialect, native: dialect !== "none" };
|
|
417
467
|
};
|
|
418
468
|
let { dialect: toolDialect, native: nativeToolsActive } = resolveNativeTools(provider, model);
|
|
419
|
-
const selectToolDefs = (native, compact) => {
|
|
469
|
+
const selectToolDefs = (native, compact, routeProvider = provider, routeModel = model) => {
|
|
420
470
|
if (!native)
|
|
421
471
|
return undefined;
|
|
422
472
|
const base = compact
|
|
423
473
|
? getCompactToolDefinitions()
|
|
424
474
|
: getToolDefinitions();
|
|
425
|
-
const allow = new Set([
|
|
475
|
+
const allow = new Set([
|
|
476
|
+
...routeToolNames(routeProvider, routeModel),
|
|
477
|
+
...RUNNER_META_TOOL_NAMES,
|
|
478
|
+
]);
|
|
426
479
|
return base.filter((d) => allow.has(d.name));
|
|
427
480
|
};
|
|
428
481
|
let lastAnswer = "";
|
|
@@ -489,11 +542,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
489
542
|
// constitution (and, on Anthropic, the native tool schemas before it).
|
|
490
543
|
const buildStableSystemContent = (native) => {
|
|
491
544
|
const reliability = getReliabilityPolicy();
|
|
545
|
+
const visionAvailable = modelSupportsVision(provider, model);
|
|
492
546
|
return (useCompactSystemPrompt
|
|
493
547
|
? renderCompactAgentSystemPrompt
|
|
494
|
-
: renderAgentSystemPrompt)(
|
|
548
|
+
: renderAgentSystemPrompt)(routeToolNames(provider, model).join(", "), {
|
|
495
549
|
nativeTools: native,
|
|
496
550
|
stableEnvironment: true,
|
|
551
|
+
imageView: visionAvailable,
|
|
497
552
|
// E6: slim native constitution when API tool schemas are attached.
|
|
498
553
|
...(native ? { slimNative: reliability.slimNativePrompt } : {}),
|
|
499
554
|
});
|
|
@@ -852,6 +907,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
852
907
|
// reset on any successful stream so each failure episode gets a fresh
|
|
853
908
|
// budget and we only give up in the worst case.
|
|
854
909
|
let allowModelFallback = false;
|
|
910
|
+
let preferModelFallback = false;
|
|
855
911
|
const recoveryState = createStreamRecoveryState();
|
|
856
912
|
// Track tool calls truncated by the token limit so we can ask the model
|
|
857
913
|
// to retry in smaller pieces instead of leaking broken JSON as an answer.
|
|
@@ -2028,6 +2084,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2028
2084
|
},
|
|
2029
2085
|
confirmed: true,
|
|
2030
2086
|
userPrompt: prompt,
|
|
2087
|
+
// image.view needs the active route to check vision support and size
|
|
2088
|
+
// images to the provider's per-image budget.
|
|
2089
|
+
llmProvider: provider,
|
|
2090
|
+
llmModel: model,
|
|
2031
2091
|
sessionId: session.sessionId,
|
|
2032
2092
|
...(delegation?.taskId ? { taskId: delegation.taskId } : {}),
|
|
2033
2093
|
...(delegation ? { delegationId: delegation.id } : {}),
|
|
@@ -3006,6 +3066,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3006
3066
|
options.signal?.throwIfAborted();
|
|
3007
3067
|
let call;
|
|
3008
3068
|
let assistantText;
|
|
3069
|
+
let canonicalAssistantVisible = "";
|
|
3009
3070
|
let recoveredFromBareJson = false;
|
|
3010
3071
|
if (pendingCalls.length > 0) {
|
|
3011
3072
|
call = pendingCalls.shift();
|
|
@@ -3117,6 +3178,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3117
3178
|
provider,
|
|
3118
3179
|
model,
|
|
3119
3180
|
allowModelFallback,
|
|
3181
|
+
preferModelFallback,
|
|
3120
3182
|
messages,
|
|
3121
3183
|
// Sampling is provider/model policy (llm/sampling.ts).
|
|
3122
3184
|
// Sending a fixed 0.2 here overrode it for every model.
|
|
@@ -3273,6 +3335,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3273
3335
|
// starts fresh (and we never give up while making progress).
|
|
3274
3336
|
resetStreamRecoveryState(recoveryState);
|
|
3275
3337
|
allowModelFallback = false;
|
|
3338
|
+
preferModelFallback = false;
|
|
3276
3339
|
}
|
|
3277
3340
|
catch (streamError) {
|
|
3278
3341
|
// User cancelled (double-Esc) — never try to recover, just stop.
|
|
@@ -3311,15 +3374,16 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3311
3374
|
deltaParser?.finish();
|
|
3312
3375
|
const partial = rememberThinkingFromText(accumulatedText);
|
|
3313
3376
|
const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
|
|
3314
|
-
const
|
|
3377
|
+
const rawPartialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible);
|
|
3378
|
+
const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
|
|
3379
|
+
const partialVisible = normalizedPartialVisible.trim();
|
|
3315
3380
|
if (partialVisible) {
|
|
3316
|
-
|
|
3317
|
-
writeAssistantMessage(partialVisible);
|
|
3318
|
-
}
|
|
3381
|
+
writeAssistantMessage(partialVisible);
|
|
3319
3382
|
pushAssistantHistory(partialVisible);
|
|
3383
|
+
interruptedVisible += normalizedPartialVisible;
|
|
3320
3384
|
}
|
|
3321
|
-
else if (!writesDirectly
|
|
3322
|
-
emit({ type: "assistant-message", text:
|
|
3385
|
+
else if (!writesDirectly) {
|
|
3386
|
+
emit({ type: "assistant-message", text: "" });
|
|
3323
3387
|
}
|
|
3324
3388
|
if (partial.hasThinking && !hasShownToolCall) {
|
|
3325
3389
|
writeThinkingBlock(partial.thinkContent);
|
|
@@ -3347,6 +3411,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3347
3411
|
retryWithoutThinking = true;
|
|
3348
3412
|
if (plan.allowModelFallback)
|
|
3349
3413
|
allowModelFallback = true;
|
|
3414
|
+
if (plan.preferModelFallback)
|
|
3415
|
+
preferModelFallback = true;
|
|
3350
3416
|
if (plan.forceCompact) {
|
|
3351
3417
|
await maybeAutoCompact(`stream-recovery:${failureKind}`, true);
|
|
3352
3418
|
}
|
|
@@ -3412,11 +3478,16 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3412
3478
|
const usedNativeProtocol = Boolean(completion.toolCalls?.length) ||
|
|
3413
3479
|
(toolsAttached && !isTextOnlyModel(provider, model));
|
|
3414
3480
|
const assistantTextResult = rememberThinkingFromText(completion.text);
|
|
3415
|
-
|
|
3481
|
+
const continuedVisible = trimExactContinuationOverlap(interruptedVisible, assistantTextResult.visible);
|
|
3482
|
+
canonicalAssistantVisible = interruptedVisible + continuedVisible;
|
|
3483
|
+
assistantText = {
|
|
3484
|
+
...assistantTextResult,
|
|
3485
|
+
visible: continuedVisible,
|
|
3486
|
+
};
|
|
3416
3487
|
const commitAssistantRetry = (historyText) => {
|
|
3417
3488
|
const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
|
|
3418
3489
|
if (!hasShownToolCall) {
|
|
3419
|
-
const displayText = textBeforeToolCall(
|
|
3490
|
+
const displayText = textBeforeToolCall(collapseRepeatedText(assistantText.visible)).trim();
|
|
3420
3491
|
if (displayText) {
|
|
3421
3492
|
writeAssistantMessage(displayText);
|
|
3422
3493
|
}
|
|
@@ -3431,6 +3502,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3431
3502
|
}
|
|
3432
3503
|
}
|
|
3433
3504
|
pushAssistantHistory(historyText);
|
|
3505
|
+
interruptedVisible = "";
|
|
3434
3506
|
};
|
|
3435
3507
|
// Only emit a thinking-block event when the classic renderer is
|
|
3436
3508
|
// active (writesDirectly / no deltaParser). In TUI v2 the
|
|
@@ -3600,7 +3672,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3600
3672
|
}
|
|
3601
3673
|
}
|
|
3602
3674
|
}
|
|
3603
|
-
if (!
|
|
3675
|
+
if (!canonicalAssistantVisible.trim() && !call) {
|
|
3604
3676
|
emptyVisibleRetries += 1;
|
|
3605
3677
|
if (emptyVisibleRetries <= 3) {
|
|
3606
3678
|
if (assistantText.hasThinking) {
|
|
@@ -3611,7 +3683,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3611
3683
|
}
|
|
3612
3684
|
if (assistantText.hasThinking)
|
|
3613
3685
|
retryWithoutThinking = true;
|
|
3614
|
-
commitAssistantRetry(
|
|
3686
|
+
commitAssistantRetry(assistantText.visible);
|
|
3615
3687
|
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
3616
3688
|
const buildNudge = freshWebSearchRequired && !sawFreshWebSearch
|
|
3617
3689
|
? toolsAttached
|
|
@@ -3817,7 +3889,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3817
3889
|
}
|
|
3818
3890
|
// Exhausted retries — fall through to the normal path.
|
|
3819
3891
|
}
|
|
3820
|
-
const
|
|
3892
|
+
const displayCleaned = collapseRepeatedText(stripSentinelTokens(assistantText.visible));
|
|
3893
|
+
const cleaned = collapseRepeatedText(stripSentinelTokens(canonicalAssistantVisible));
|
|
3821
3894
|
const narratedAction = looksLikeActionNarration(cleaned);
|
|
3822
3895
|
const narratedWebAction = looksLikeWebActionNarration(cleaned);
|
|
3823
3896
|
const reconciledPlanAtCompletion = await reconcileOpenTaskBeforeFinalizing();
|
|
@@ -4094,7 +4167,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4094
4167
|
? "One or more required plan tasks failed."
|
|
4095
4168
|
: outcomeStatus === "partial"
|
|
4096
4169
|
? "Required outcome criteria remain unsupported by current evidence."
|
|
4097
|
-
: undefined);
|
|
4170
|
+
: undefined, displayCleaned);
|
|
4098
4171
|
}
|
|
4099
4172
|
// A valid primary tool call exists for this fresh model turn. Show any
|
|
4100
4173
|
// prose / thinking that preceded it, record the assistant message ONCE.
|
|
@@ -4103,10 +4176,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4103
4176
|
: nativeToolCalls.length
|
|
4104
4177
|
? assistantText.visible.trim()
|
|
4105
4178
|
: textBeforeToolCall(assistantText.visible);
|
|
4106
|
-
if (beforeTool
|
|
4107
|
-
!deferredToolCalls.some((entry) => entry.shown)) {
|
|
4179
|
+
if (beforeTool) {
|
|
4108
4180
|
writeAssistantMessage(beforeTool);
|
|
4109
4181
|
}
|
|
4182
|
+
else if (deltaParser) {
|
|
4183
|
+
emit({ type: "assistant-message", text: "" });
|
|
4184
|
+
}
|
|
4185
|
+
interruptedVisible = "";
|
|
4110
4186
|
let bound = [];
|
|
4111
4187
|
if (nativeToolCalls.length) {
|
|
4112
4188
|
bound = nativeToolCalls.map((tc, index) => {
|
|
@@ -4381,6 +4457,23 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4381
4457
|
content: toolContent,
|
|
4382
4458
|
});
|
|
4383
4459
|
}
|
|
4460
|
+
// image.view hands back real image bytes. Tool results are text-only
|
|
4461
|
+
// on every provider wire, and images are only serialized on user
|
|
4462
|
+
// turns, so the bytes ride a deferred internal user message that
|
|
4463
|
+
// lands after the assistant→tool group is closed — inserting it here
|
|
4464
|
+
// would orphan the remaining tool results.
|
|
4465
|
+
if (res.result.images?.length) {
|
|
4466
|
+
deferredPostToolMessages.push({
|
|
4467
|
+
role: "user",
|
|
4468
|
+
internal: true,
|
|
4469
|
+
content: `[${res.call.name}] The ${res.result.images.length === 1 ? "image" : `${res.result.images.length} images`} you asked to look at ` +
|
|
4470
|
+
`${res.result.images.length === 1 ? "is" : "are"} attached to this message` +
|
|
4471
|
+
`${res.result.images.length === 1 ? "" : ", in the order you requested them"}: ` +
|
|
4472
|
+
`${res.result.images.map((image) => image.path ?? "(unnamed)").join(", ")}. ` +
|
|
4473
|
+
"Judge them from the pixels and continue the task.",
|
|
4474
|
+
images: res.result.images,
|
|
4475
|
+
});
|
|
4476
|
+
}
|
|
4384
4477
|
// Reset retry counters — they track consecutive failures, not cumulative.
|
|
4385
4478
|
truncatedToolRetries = 0;
|
|
4386
4479
|
malformedFenceRetries = 0;
|