@pentoshi/clai 3.10.0 → 3.11.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/dist/agent/runner.js +145 -49
- package/dist/agent/runner.js.map +1 -1
- package/dist/llm/http.d.ts +3 -15
- package/dist/llm/http.js +14 -22
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/router.js +17 -3
- package/dist/llm/router.js.map +1 -1
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/system.agent.md +5 -5
- package/dist/tools/command-intent.js +2 -3
- package/dist/tools/command-intent.js.map +1 -1
- package/dist/tools/definitions.js +14 -6
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/registry.js +6 -12
- package/dist/tools/registry.js.map +1 -1
- package/dist/tui-v2/actions/action-id.d.ts +1 -1
- package/dist/tui-v2/actions/action-id.js +1 -0
- package/dist/tui-v2/actions/action-id.js.map +1 -1
- package/dist/tui-v2/actions/format-shortcuts.js +1 -0
- package/dist/tui-v2/actions/format-shortcuts.js.map +1 -1
- package/dist/tui-v2/actions/keymap.js +2 -0
- package/dist/tui-v2/actions/keymap.js.map +1 -1
- package/dist/tui-v2/app/App.js +3 -1
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/bootstrap/console-guard.d.ts +20 -0
- package/dist/tui-v2/bootstrap/console-guard.js +73 -0
- package/dist/tui-v2/bootstrap/console-guard.js.map +1 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js +14 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
- package/dist/tui-v2/components/composer/composer-input-box.js +5 -1
- package/dist/tui-v2/components/composer/composer-input-box.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.d.ts +8 -2
- package/dist/tui-v2/components/status/status-line.js +12 -8
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/components/transcript/thinking-block.d.ts +3 -1
- package/dist/tui-v2/components/transcript/thinking-block.js +49 -5
- package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-row.js +1 -1
- package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
- package/dist/tui-v2/composer/composer-action-port.d.ts +15 -3
- package/dist/tui-v2/composer/composer-action-port.js +49 -6
- package/dist/tui-v2/composer/composer-action-port.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +37 -44
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/tui-v2/composer/draft-actions.d.ts +31 -0
- package/dist/tui-v2/composer/draft-actions.js +45 -0
- package/dist/tui-v2/composer/draft-actions.js.map +1 -0
- package/dist/tui-v2/composer/textarea-keybindings.d.ts +1 -1
- package/dist/tui-v2/composer/textarea-keybindings.js +33 -0
- package/dist/tui-v2/composer/textarea-keybindings.js.map +1 -1
- package/dist/tui-v2/composer/use-draft-actions.d.ts +32 -0
- package/dist/tui-v2/composer/use-draft-actions.js +59 -0
- package/dist/tui-v2/composer/use-draft-actions.js.map +1 -0
- package/dist/tui-v2/state/transcript-reducer.js +15 -0
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
- package/dist/tui-v2/state/use-has-draft.d.ts +6 -0
- package/dist/tui-v2/state/use-has-draft.js +13 -0
- package/dist/tui-v2/state/use-has-draft.js.map +1 -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
|
@@ -4,7 +4,7 @@ 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
6
|
import { resolveToolDialect } from "../llm/capabilities.js";
|
|
7
|
-
import { syntheticToolCallId, isTextOnlyModel, fromWireName, } from "../llm/tool-protocol.js";
|
|
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";
|
|
10
10
|
import { jobManager, } from "../tools/jobs.js";
|
|
@@ -1154,6 +1154,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1154
1154
|
// normal continuation is governed by evidence and resource deltas above.
|
|
1155
1155
|
const maxIterations = Math.max(210, computeMaxIterations(stepBudget));
|
|
1156
1156
|
let productiveSteps = 0;
|
|
1157
|
+
let consecutiveModelOnlyRounds = 0;
|
|
1157
1158
|
/** Successful file mutation this turn — kills false "error diagnosed but not fixed". */
|
|
1158
1159
|
let sawSuccessfulMutation = false;
|
|
1159
1160
|
let step = -1;
|
|
@@ -3137,7 +3138,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3137
3138
|
return;
|
|
3138
3139
|
const name = fromWireName(delta.name) ?? delta.name;
|
|
3139
3140
|
const existing = deferredToolCalls[delta.index];
|
|
3140
|
-
if (existing) {
|
|
3141
|
+
if (existing && existing.call.name !== "…") {
|
|
3141
3142
|
if (delta.argumentsBytes &&
|
|
3142
3143
|
delta.argumentsBytes >= 4096 &&
|
|
3143
3144
|
!writesDirectly) {
|
|
@@ -3148,20 +3149,23 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3148
3149
|
}
|
|
3149
3150
|
return;
|
|
3150
3151
|
}
|
|
3151
|
-
// Ensure slots are dense so index maps to deferredToolCalls[i].
|
|
3152
3152
|
while (deferredToolCalls.length < delta.index) {
|
|
3153
|
+
const slot = deferredToolCalls.length;
|
|
3154
|
+
const placeholderId = `tool-${++nextToolEventId}`;
|
|
3155
|
+
callIds[slot] = placeholderId;
|
|
3153
3156
|
deferredToolCalls.push({
|
|
3154
|
-
eventId:
|
|
3157
|
+
eventId: placeholderId,
|
|
3155
3158
|
call: { name: "…", args: {} },
|
|
3156
3159
|
rendered: "",
|
|
3160
|
+
shown: false,
|
|
3157
3161
|
});
|
|
3158
3162
|
}
|
|
3159
3163
|
const call = normalizeToolCall({
|
|
3160
3164
|
name,
|
|
3161
3165
|
args: {},
|
|
3162
3166
|
});
|
|
3163
|
-
const eventId = `tool-${++nextToolEventId}`;
|
|
3164
|
-
callIds.
|
|
3167
|
+
const eventId = existing?.eventId ?? `tool-${++nextToolEventId}`;
|
|
3168
|
+
callIds[delta.index] = eventId;
|
|
3165
3169
|
alreadyPrintedIds.add(eventId);
|
|
3166
3170
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
3167
3171
|
chalk.gray(` ${formatToolArgs(call)}`);
|
|
@@ -3169,6 +3173,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3169
3173
|
eventId,
|
|
3170
3174
|
call,
|
|
3171
3175
|
rendered: styleToolChatter(call, toolCallLine) + "\n",
|
|
3176
|
+
shown: false,
|
|
3172
3177
|
};
|
|
3173
3178
|
if (deferredToolCalls.length === delta.index) {
|
|
3174
3179
|
deferredToolCalls.push(entry);
|
|
@@ -3178,6 +3183,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3178
3183
|
}
|
|
3179
3184
|
streamedCallsCount = Math.max(streamedCallsCount, deferredToolCalls.length);
|
|
3180
3185
|
if (!writesDirectly) {
|
|
3186
|
+
writeToolCall(eventId, call, entry.rendered);
|
|
3187
|
+
entry.shown = true;
|
|
3181
3188
|
emit({ type: "status", text: call.name });
|
|
3182
3189
|
}
|
|
3183
3190
|
else {
|
|
@@ -3202,16 +3209,20 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3202
3209
|
while (streamedCallsCount < parsedCalls.length) {
|
|
3203
3210
|
const call = parsedCalls[streamedCallsCount];
|
|
3204
3211
|
const eventId = `tool-${++nextToolEventId}`;
|
|
3205
|
-
callIds
|
|
3212
|
+
callIds[streamedCallsCount] = eventId;
|
|
3206
3213
|
alreadyPrintedIds.add(eventId);
|
|
3207
3214
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
3208
3215
|
chalk.gray(` ${formatToolArgs(call)}`);
|
|
3209
|
-
|
|
3216
|
+
const entry = {
|
|
3210
3217
|
eventId,
|
|
3211
3218
|
call,
|
|
3212
3219
|
rendered: styleToolChatter(call, toolCallLine) + "\n",
|
|
3213
|
-
|
|
3220
|
+
shown: false,
|
|
3221
|
+
};
|
|
3222
|
+
deferredToolCalls.push(entry);
|
|
3214
3223
|
if (!writesDirectly) {
|
|
3224
|
+
writeToolCall(eventId, call, entry.rendered);
|
|
3225
|
+
entry.shown = true;
|
|
3215
3226
|
emit({ type: "status", text: call.name });
|
|
3216
3227
|
}
|
|
3217
3228
|
streamedCallsCount += 1;
|
|
@@ -3293,17 +3304,42 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3293
3304
|
kind: failureKind,
|
|
3294
3305
|
state: recoveryState,
|
|
3295
3306
|
});
|
|
3307
|
+
const partialStream = streamAlreadyEmitted(streamError) || accumulatedText.length > 0;
|
|
3308
|
+
let continuationNudge = "";
|
|
3309
|
+
if (partialStream) {
|
|
3310
|
+
spinner.stop();
|
|
3311
|
+
deltaParser?.finish();
|
|
3312
|
+
const partial = rememberThinkingFromText(accumulatedText);
|
|
3313
|
+
const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
|
|
3314
|
+
const partialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible).trim();
|
|
3315
|
+
if (partialVisible) {
|
|
3316
|
+
if (!hasShownToolCall) {
|
|
3317
|
+
writeAssistantMessage(partialVisible);
|
|
3318
|
+
}
|
|
3319
|
+
pushAssistantHistory(partialVisible);
|
|
3320
|
+
}
|
|
3321
|
+
else if (!writesDirectly && !hasShownToolCall) {
|
|
3322
|
+
emit({ type: "assistant-message", text: partial.visible });
|
|
3323
|
+
}
|
|
3324
|
+
if (partial.hasThinking && !hasShownToolCall) {
|
|
3325
|
+
writeThinkingBlock(partial.thinkContent);
|
|
3326
|
+
}
|
|
3327
|
+
for (const deferred of deferredToolCalls) {
|
|
3328
|
+
if (!deferred.shown || deferred.call.name === "…")
|
|
3329
|
+
continue;
|
|
3330
|
+
writeToolBlocked(deferred.eventId, deferred.call.name, "Incomplete tool call discarded after the provider stream was interrupted.", chalk.yellow(" ⚠ incomplete tool call discarded after stream interruption\n"));
|
|
3331
|
+
}
|
|
3332
|
+
continuationNudge =
|
|
3333
|
+
"The provider stream was interrupted after partial output. Continue from the exact stopping point without repeating prior text. Any incomplete tool call was discarded and must be reissued in full.";
|
|
3334
|
+
const restartNotice = plan.action === "give-up"
|
|
3335
|
+
? "partial response preserved before terminal provider failure"
|
|
3336
|
+
: "partial response preserved — resuming from the interruption";
|
|
3337
|
+
writeNotice("warn", restartNotice, chalk.yellow(` ⚠ ${restartNotice}\n`));
|
|
3338
|
+
}
|
|
3296
3339
|
if (plan.action === "give-up") {
|
|
3297
3340
|
throw streamError;
|
|
3298
3341
|
}
|
|
3299
3342
|
recordRecoveryAttempt(recoveryState, failureKind);
|
|
3300
|
-
// The router refuses transparent retries after emission,
|
|
3301
|
-
// but the recovery ladder may still re-run the step. Say so, since
|
|
3302
|
-
// the visible answer restarts from scratch.
|
|
3303
|
-
if (streamAlreadyEmitted(streamError)) {
|
|
3304
|
-
const restartNotice = "partial answer discarded after a mid-stream failure — the reply restarts below";
|
|
3305
|
-
writeNotice("warn", restartNotice, chalk.yellow(` ⚠ ${restartNotice}\n`));
|
|
3306
|
-
}
|
|
3307
3343
|
if (plan.notice) {
|
|
3308
3344
|
writeNotice("warn", plan.notice, chalk.yellow(` ⚠ ${plan.notice}\n`));
|
|
3309
3345
|
}
|
|
@@ -3314,8 +3350,11 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3314
3350
|
if (plan.forceCompact) {
|
|
3315
3351
|
await maybeAutoCompact(`stream-recovery:${failureKind}`, true);
|
|
3316
3352
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3353
|
+
const recoveryNudge = [continuationNudge, plan.nudge]
|
|
3354
|
+
.filter((part) => Boolean(part))
|
|
3355
|
+
.join("\n\n");
|
|
3356
|
+
if (recoveryNudge) {
|
|
3357
|
+
messages.push(recoveryUserMessage(recoveryNudge));
|
|
3319
3358
|
}
|
|
3320
3359
|
if (plan.delayMs > 0) {
|
|
3321
3360
|
emit({
|
|
@@ -3374,6 +3413,25 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3374
3413
|
(toolsAttached && !isTextOnlyModel(provider, model));
|
|
3375
3414
|
const assistantTextResult = rememberThinkingFromText(completion.text);
|
|
3376
3415
|
assistantText = assistantTextResult;
|
|
3416
|
+
const commitAssistantRetry = (historyText) => {
|
|
3417
|
+
const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
|
|
3418
|
+
if (!hasShownToolCall) {
|
|
3419
|
+
const displayText = textBeforeToolCall(stripThinking(collapseRepeatedText(completion.text)).visible).trim();
|
|
3420
|
+
if (displayText) {
|
|
3421
|
+
writeAssistantMessage(displayText);
|
|
3422
|
+
}
|
|
3423
|
+
else if (deltaParser) {
|
|
3424
|
+
emit({ type: "assistant-message", text: "" });
|
|
3425
|
+
}
|
|
3426
|
+
if (assistantText.hasThinking && deltaParser) {
|
|
3427
|
+
emit({
|
|
3428
|
+
type: "thinking-block",
|
|
3429
|
+
content: assistantText.thinkContent,
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
pushAssistantHistory(historyText);
|
|
3434
|
+
};
|
|
3377
3435
|
// Only emit a thinking-block event when the classic renderer is
|
|
3378
3436
|
// active (writesDirectly / no deltaParser). In TUI v2 the
|
|
3379
3437
|
// deltaParser already streamed thinking-delta events that created
|
|
@@ -3392,13 +3450,14 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3392
3450
|
// otherwise create cards now (non-streaming / name-after-done providers).
|
|
3393
3451
|
if (nativeToolCalls.length) {
|
|
3394
3452
|
if (deferredToolCalls.length === 0) {
|
|
3395
|
-
for (
|
|
3453
|
+
for (let i = 0; i < nativeToolCalls.length; i += 1) {
|
|
3454
|
+
const tc = nativeToolCalls[i];
|
|
3396
3455
|
const normalized = normalizeToolCall({
|
|
3397
3456
|
name: tc.name,
|
|
3398
3457
|
args: tc.args,
|
|
3399
3458
|
});
|
|
3400
3459
|
const eventId = `tool-${++nextToolEventId}`;
|
|
3401
|
-
callIds
|
|
3460
|
+
callIds[i] = eventId;
|
|
3402
3461
|
alreadyPrintedIds.add(eventId);
|
|
3403
3462
|
const toolCallLine = chalk.cyan(` ▶ ${normalized.name}`) +
|
|
3404
3463
|
chalk.gray(` ${formatToolArgs(normalized)}`);
|
|
@@ -3406,6 +3465,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3406
3465
|
eventId,
|
|
3407
3466
|
call: normalized,
|
|
3408
3467
|
rendered: styleToolChatter(normalized, toolCallLine) + "\n",
|
|
3468
|
+
shown: false,
|
|
3409
3469
|
});
|
|
3410
3470
|
}
|
|
3411
3471
|
}
|
|
@@ -3418,6 +3478,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3418
3478
|
});
|
|
3419
3479
|
const existing = deferredToolCalls[i];
|
|
3420
3480
|
if (existing && existing.call.name !== "…") {
|
|
3481
|
+
callIds[i] = existing.eventId;
|
|
3421
3482
|
existing.call = normalized;
|
|
3422
3483
|
const toolCallLine = chalk.cyan(` ▶ ${normalized.name}`) +
|
|
3423
3484
|
chalk.gray(` ${formatToolArgs(normalized)}`);
|
|
@@ -3426,16 +3487,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3426
3487
|
}
|
|
3427
3488
|
else if (!existing || existing.call.name === "…") {
|
|
3428
3489
|
const eventId = existing?.eventId ?? `tool-${++nextToolEventId}`;
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
alreadyPrintedIds.add(eventId);
|
|
3432
|
-
}
|
|
3490
|
+
callIds[i] = eventId;
|
|
3491
|
+
alreadyPrintedIds.add(eventId);
|
|
3433
3492
|
const toolCallLine = chalk.cyan(` ▶ ${normalized.name}`) +
|
|
3434
3493
|
chalk.gray(` ${formatToolArgs(normalized)}`);
|
|
3435
3494
|
const entry = {
|
|
3436
3495
|
eventId,
|
|
3437
3496
|
call: normalized,
|
|
3438
3497
|
rendered: styleToolChatter(normalized, toolCallLine) + "\n",
|
|
3498
|
+
shown: existing?.shown ?? false,
|
|
3439
3499
|
};
|
|
3440
3500
|
if (existing)
|
|
3441
3501
|
deferredToolCalls[i] = entry;
|
|
@@ -3551,7 +3611,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3551
3611
|
}
|
|
3552
3612
|
if (assistantText.hasThinking)
|
|
3553
3613
|
retryWithoutThinking = true;
|
|
3554
|
-
|
|
3614
|
+
commitAssistantRetry(stripThinking(collapseRepeatedText(completion.text)).visible);
|
|
3555
3615
|
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
3556
3616
|
const buildNudge = freshWebSearchRequired && !sawFreshWebSearch
|
|
3557
3617
|
? toolsAttached
|
|
@@ -3603,6 +3663,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3603
3663
|
}
|
|
3604
3664
|
}
|
|
3605
3665
|
if (!call) {
|
|
3666
|
+
consecutiveModelOnlyRounds += 1;
|
|
3606
3667
|
if (bareArgsOnly) {
|
|
3607
3668
|
bareToolJsonRetries += 1;
|
|
3608
3669
|
if (bareToolJsonRetries <= 3) {
|
|
@@ -3611,7 +3672,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3611
3672
|
: "tool call missing its name/fence — asking the model to re-emit a proper ```tool block", chalk.yellow(toolsAttached
|
|
3612
3673
|
? " ⚠ tool call missing its name — asking the model to call a tool properly\n"
|
|
3613
3674
|
: " ⚠ tool call missing its name/fence — asking the model to re-emit a proper ```tool block\n"));
|
|
3614
|
-
|
|
3675
|
+
commitAssistantRetry(assistantText.visible);
|
|
3615
3676
|
messages.push(recoveryUserMessage(isPlanMode && !activePlan
|
|
3616
3677
|
? toolsAttached
|
|
3617
3678
|
? "Your previous message was a bare JSON args object with no tool name, so NOTHING ran. " +
|
|
@@ -3634,7 +3695,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3634
3695
|
}
|
|
3635
3696
|
if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin\|>/i.test(assistantText.visible)) {
|
|
3636
3697
|
writeNotice("warn", "tool call was malformed or cut off — asking the model to retry in JSON form", chalk.yellow(" ⚠ tool call was malformed or cut off — asking the model to retry in JSON form\n"));
|
|
3637
|
-
|
|
3698
|
+
commitAssistantRetry(assistantText.visible);
|
|
3638
3699
|
messages.push(recoveryUserMessage(toolsAttached
|
|
3639
3700
|
? "Your previous tool call was malformed or truncated. " +
|
|
3640
3701
|
toolNudge(true) +
|
|
@@ -3656,7 +3717,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3656
3717
|
if (writeResult.ok) {
|
|
3657
3718
|
const lineCount = salvaged.content.split("\n").length;
|
|
3658
3719
|
writeNotice("info", `tool call was truncated — salvaged ${lineCount} lines and wrote to ${salvaged.path}`, chalk.cyan(` ℹ tool call was truncated — salvaged ${lineCount} lines to ${salvaged.path}\n`));
|
|
3659
|
-
|
|
3720
|
+
commitAssistantRetry(stripThinking(assistantText.visible).visible);
|
|
3660
3721
|
const priorBytes = writeResult.bytesOnDisk;
|
|
3661
3722
|
const salvagedToolName = salvaged.operation === "append" ? "fs.append" : "fs.write";
|
|
3662
3723
|
messages.push({
|
|
@@ -3685,7 +3746,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3685
3746
|
}
|
|
3686
3747
|
if (truncatedToolRetries <= 3) {
|
|
3687
3748
|
writeNotice("warn", "tool call was cut off (output too long) — asking the model to retry safely", chalk.yellow(" ⚠ tool call was cut off (output too long) — asking the model to retry safely\n"));
|
|
3688
|
-
|
|
3749
|
+
commitAssistantRetry(stripThinking(assistantText.visible).visible);
|
|
3689
3750
|
messages.push({
|
|
3690
3751
|
role: "user",
|
|
3691
3752
|
content: toolsAttached
|
|
@@ -3718,7 +3779,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3718
3779
|
if (writeResult.ok) {
|
|
3719
3780
|
const lineCount = salvaged.content.split("\n").length;
|
|
3720
3781
|
writeNotice("info", `malformed tool call salvaged — wrote ${lineCount} lines to ${salvaged.path}`, chalk.cyan(` ℹ malformed tool call salvaged — wrote ${lineCount} lines to ${salvaged.path}\n`));
|
|
3721
|
-
|
|
3782
|
+
commitAssistantRetry(stripThinking(assistantText.visible).visible);
|
|
3722
3783
|
messages.push({
|
|
3723
3784
|
role: "user",
|
|
3724
3785
|
content: `The system extracted and wrote ${lineCount} lines to ${salvaged.path} from your malformed tool call. ` +
|
|
@@ -3736,7 +3797,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3736
3797
|
malformedFenceRetries += 1;
|
|
3737
3798
|
if (malformedFenceRetries <= 3) {
|
|
3738
3799
|
writeNotice("warn", "tool block present but its JSON didn't parse — asking the model to re-emit valid JSON", chalk.yellow(" ⚠ tool block present but its JSON didn't parse — asking the model to re-emit valid JSON\n"));
|
|
3739
|
-
|
|
3800
|
+
commitAssistantRetry(stripThinking(assistantText.visible).visible);
|
|
3740
3801
|
messages.push({
|
|
3741
3802
|
role: "user",
|
|
3742
3803
|
content: toolsAttached
|
|
@@ -3756,14 +3817,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3756
3817
|
}
|
|
3757
3818
|
// Exhausted retries — fall through to the normal path.
|
|
3758
3819
|
}
|
|
3759
|
-
const cleaned = stripSentinelTokens(assistantText.visible);
|
|
3760
|
-
if (unreadResponderNotificationIds.size > 0) {
|
|
3761
|
-
const unread = [...unreadResponderNotificationIds];
|
|
3762
|
-
pushAssistantHistory(assistantText.visible);
|
|
3763
|
-
messages.push(recoveryUserMessage(`You have ${unread.length} delivered Responder result(s) that remain unread: ${unread.join(", ")}. ` +
|
|
3764
|
-
"If analysis is incomplete, call only the bounded evidence tool needed now. If each result has been analyzed and is satisfactory, you MUST call job.read with its jobId or exact notificationId before giving a final response. job.read does not require an active plan; do not create or update a plan merely to acknowledge a result."));
|
|
3765
|
-
continue;
|
|
3766
|
-
}
|
|
3820
|
+
const cleaned = collapseRepeatedText(stripSentinelTokens(assistantText.visible));
|
|
3767
3821
|
const narratedAction = looksLikeActionNarration(cleaned);
|
|
3768
3822
|
const narratedWebAction = looksLikeWebActionNarration(cleaned);
|
|
3769
3823
|
const reconciledPlanAtCompletion = await reconcileOpenTaskBeforeFinalizing();
|
|
@@ -3778,11 +3832,47 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3778
3832
|
(!informationalQuery &&
|
|
3779
3833
|
!idleOrSocialPrompt &&
|
|
3780
3834
|
(buildLikeTurn || pentestLikeTurn));
|
|
3835
|
+
const unreadResponderResults = unreadResponderNotificationIds.size > 0;
|
|
3781
3836
|
const wantsAction = !completedPlanDuringThisTurn &&
|
|
3782
3837
|
!idleOrSocialPrompt &&
|
|
3783
3838
|
(userExpectsWork ||
|
|
3784
3839
|
(narratedAction && !informationalQuery) ||
|
|
3785
3840
|
(narratedWebAction && !informationalQuery));
|
|
3841
|
+
if ((wantsAction || unreadResponderResults) &&
|
|
3842
|
+
toolsAttached &&
|
|
3843
|
+
consecutiveModelOnlyRounds === 2) {
|
|
3844
|
+
markTextOnlyModel(provider, model);
|
|
3845
|
+
commitAssistantRetry(assistantText.visible);
|
|
3846
|
+
writeNotice("warn", "model repeatedly returned prose instead of a native tool call — switching this model to the text tool protocol", chalk.yellow(" ⚠ switching this model to the text tool protocol after repeated non-actionable responses\n"));
|
|
3847
|
+
messages.push(recoveryUserMessage("Native tool calling did not produce an executable call. Continue now with exactly one complete fenced ```tool block. Do not repeat the prior narration."));
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
if ((wantsAction || unreadResponderResults) &&
|
|
3851
|
+
consecutiveModelOnlyRounds >= 6) {
|
|
3852
|
+
commitAssistantRetry(assistantText.visible);
|
|
3853
|
+
const stalledMessage = "Stopped a repeated model-only retry cycle after the model returned no executable tool call. Completed work and transcript output were preserved.";
|
|
3854
|
+
writeAssistantMessage(stalledMessage);
|
|
3855
|
+
const remainingCriteria = livePlanAtCompletion
|
|
3856
|
+
? foregroundRemaining(livePlanAtCompletion).map((task) => `[${task.id}] ${task.title}`)
|
|
3857
|
+
: [];
|
|
3858
|
+
if (unreadResponderResults) {
|
|
3859
|
+
remainingCriteria.push("Analyze and acknowledge each delivered Responder result.");
|
|
3860
|
+
}
|
|
3861
|
+
if (remainingCriteria.length === 0) {
|
|
3862
|
+
remainingCriteria.push("Continue the unfinished work with an executable tool call.");
|
|
3863
|
+
}
|
|
3864
|
+
outcomeState.outcome.status = "partial";
|
|
3865
|
+
await saveOutcomeState(outcomeState);
|
|
3866
|
+
moveTurn("partial", "repeated model-only responses");
|
|
3867
|
+
return finishTurn(stalledMessage, productiveSteps, "partial", remainingCriteria, "The model returned six consecutive responses without executing a tool.");
|
|
3868
|
+
}
|
|
3869
|
+
if (unreadResponderResults) {
|
|
3870
|
+
const unread = [...unreadResponderNotificationIds];
|
|
3871
|
+
commitAssistantRetry(assistantText.visible);
|
|
3872
|
+
messages.push(recoveryUserMessage(`You have ${unread.length} delivered Responder result(s) that remain unread: ${unread.join(", ")}. ` +
|
|
3873
|
+
"If analysis is incomplete, call only the bounded evidence tool needed now. If each result has been analyzed and is satisfactory, you MUST call job.read with its jobId or exact notificationId before giving a final response. job.read does not require an active plan; do not create or update a plan merely to acknowledge a result."));
|
|
3874
|
+
continue;
|
|
3875
|
+
}
|
|
3786
3876
|
const planNarrated = (buildLikeTurn || pentestLikeTurn) &&
|
|
3787
3877
|
!activePlan &&
|
|
3788
3878
|
looksLikePlanNarration(cleaned);
|
|
@@ -3834,7 +3924,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3834
3924
|
}
|
|
3835
3925
|
if (action) {
|
|
3836
3926
|
consumeBudget(recovery, action.budgetKey);
|
|
3837
|
-
|
|
3927
|
+
commitAssistantRetry(assistantText.visible);
|
|
3838
3928
|
messages.push(recoveryUserMessage(action.message));
|
|
3839
3929
|
continue;
|
|
3840
3930
|
}
|
|
@@ -3847,7 +3937,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3847
3937
|
? " Call the web_search tool now."
|
|
3848
3938
|
: " Reply with ONLY a fenced ```tool block for web.search now."));
|
|
3849
3939
|
consumeBudget(recovery, action.budgetKey);
|
|
3850
|
-
|
|
3940
|
+
commitAssistantRetry(assistantText.visible);
|
|
3851
3941
|
messages.push(recoveryUserMessage(action.message));
|
|
3852
3942
|
continue;
|
|
3853
3943
|
}
|
|
@@ -3859,7 +3949,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3859
3949
|
if (!planAtEnd && !sawPlanCreateOk) {
|
|
3860
3950
|
const action = recoveryForMissingPlan(toolsAttached);
|
|
3861
3951
|
consumeBudget(recovery, action.budgetKey);
|
|
3862
|
-
|
|
3952
|
+
commitAssistantRetry(assistantText.visible);
|
|
3863
3953
|
messages.push(recoveryUserMessage(action.message));
|
|
3864
3954
|
continue;
|
|
3865
3955
|
}
|
|
@@ -3875,7 +3965,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3875
3965
|
budgetRemaining(recovery, "featureImpl")) {
|
|
3876
3966
|
const action = recoveryForMissingFeature(getActiveProjectRoot());
|
|
3877
3967
|
consumeBudget(recovery, action.budgetKey);
|
|
3878
|
-
|
|
3968
|
+
commitAssistantRetry(assistantText.visible);
|
|
3879
3969
|
messages.push(recoveryUserMessage(action.message));
|
|
3880
3970
|
continue;
|
|
3881
3971
|
}
|
|
@@ -3905,7 +3995,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3905
3995
|
if (codingPlanFinished || freestyleLocalAppDone) {
|
|
3906
3996
|
const action = recoveryForRuntimeVerify(getActiveProjectRoot());
|
|
3907
3997
|
consumeBudget(recovery, action.budgetKey);
|
|
3908
|
-
|
|
3998
|
+
commitAssistantRetry(assistantText.visible);
|
|
3909
3999
|
messages.push(recoveryUserMessage(action.message));
|
|
3910
4000
|
continue;
|
|
3911
4001
|
}
|
|
@@ -3920,7 +4010,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3920
4010
|
cleaned.trim().length > 0) {
|
|
3921
4011
|
const action = recoveryForFailedProbe();
|
|
3922
4012
|
consumeBudget(recovery, action.budgetKey);
|
|
3923
|
-
|
|
4013
|
+
commitAssistantRetry(assistantText.visible);
|
|
3924
4014
|
messages.push(recoveryUserMessage(action.message));
|
|
3925
4015
|
continue;
|
|
3926
4016
|
}
|
|
@@ -3932,7 +4022,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3932
4022
|
})) {
|
|
3933
4023
|
const action = recoveryForShallowPentest();
|
|
3934
4024
|
consumeBudget(recovery, action.budgetKey);
|
|
3935
|
-
|
|
4025
|
+
commitAssistantRetry(assistantText.visible);
|
|
3936
4026
|
messages.push(recoveryUserMessage(action.message));
|
|
3937
4027
|
continue;
|
|
3938
4028
|
}
|
|
@@ -3954,7 +4044,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3954
4044
|
errorFix: errorFixNarration,
|
|
3955
4045
|
});
|
|
3956
4046
|
consumeBudget(recovery, action.budgetKey);
|
|
3957
|
-
|
|
4047
|
+
commitAssistantRetry(assistantText.visible);
|
|
3958
4048
|
messages.push(recoveryUserMessage(action.message));
|
|
3959
4049
|
continue;
|
|
3960
4050
|
}
|
|
@@ -3999,6 +4089,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3999
4089
|
outcomeStatus,
|
|
4000
4090
|
remainingCriteria,
|
|
4001
4091
|
});
|
|
4092
|
+
writeAssistantMessage(cleaned);
|
|
4002
4093
|
lastAnswer = cleaned;
|
|
4003
4094
|
return finishTurn(lastAnswer, step + 1, outcomeStatus, remainingCriteria, outcomeStatus === "failed"
|
|
4004
4095
|
? "One or more required plan tasks failed."
|
|
@@ -4013,7 +4104,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4013
4104
|
: nativeToolCalls.length
|
|
4014
4105
|
? assistantText.visible.trim()
|
|
4015
4106
|
: textBeforeToolCall(assistantText.visible);
|
|
4016
|
-
if (beforeTool
|
|
4107
|
+
if (beforeTool &&
|
|
4108
|
+
!deferredToolCalls.some((entry) => entry.shown)) {
|
|
4017
4109
|
writeAssistantMessage(beforeTool);
|
|
4018
4110
|
}
|
|
4019
4111
|
let bound = [];
|
|
@@ -4182,7 +4274,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4182
4274
|
for (const deferred of activeDeferredToolCalls.slice(0, allCalls.length)) {
|
|
4183
4275
|
if (!deferred.call.name || deferred.call.name === "…")
|
|
4184
4276
|
continue;
|
|
4185
|
-
|
|
4277
|
+
if (!deferred.shown || !writesDirectly) {
|
|
4278
|
+
writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
|
|
4279
|
+
deferred.shown = true;
|
|
4280
|
+
}
|
|
4186
4281
|
}
|
|
4187
4282
|
if (historyNativeCalls.length) {
|
|
4188
4283
|
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock);
|
|
@@ -4232,6 +4327,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4232
4327
|
* explicit user abort stops remaining calls.
|
|
4233
4328
|
*/
|
|
4234
4329
|
const recordResult = (boundCall, res) => {
|
|
4330
|
+
consecutiveModelOnlyRounds = 0;
|
|
4235
4331
|
recordedNativeIds.add(boundCall.id);
|
|
4236
4332
|
actionSequenceExecuted += 1;
|
|
4237
4333
|
// A policy-suppressed call is deterministic: replaying it verbatim
|