@pentoshi/clai 3.11.1 → 3.11.3

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.
@@ -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 rendered = renderTurnOutcome(outcome, {
374
+ const renderOptions = {
344
375
  diagnostics: !suppressOutcomeDiagnostics,
345
- });
346
- writeAssistantMessage(rendered);
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, rendered));
389
+ options.onMessages(buildTurnHistory(liveMessages, displayRendered));
350
390
  }
351
391
  catch {
352
392
  // Persisting history must never break the turn.
@@ -3006,6 +3046,7 @@ export async function runAgentTurn(prompt, options = {}) {
3006
3046
  options.signal?.throwIfAborted();
3007
3047
  let call;
3008
3048
  let assistantText;
3049
+ let canonicalAssistantVisible = "";
3009
3050
  let recoveredFromBareJson = false;
3010
3051
  if (pendingCalls.length > 0) {
3011
3052
  call = pendingCalls.shift();
@@ -3311,15 +3352,16 @@ export async function runAgentTurn(prompt, options = {}) {
3311
3352
  deltaParser?.finish();
3312
3353
  const partial = rememberThinkingFromText(accumulatedText);
3313
3354
  const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
3314
- const partialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible).trim();
3355
+ const rawPartialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible);
3356
+ const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
3357
+ const partialVisible = normalizedPartialVisible.trim();
3315
3358
  if (partialVisible) {
3316
- if (!hasShownToolCall) {
3317
- writeAssistantMessage(partialVisible);
3318
- }
3359
+ writeAssistantMessage(partialVisible);
3319
3360
  pushAssistantHistory(partialVisible);
3361
+ interruptedVisible += normalizedPartialVisible;
3320
3362
  }
3321
- else if (!writesDirectly && !hasShownToolCall) {
3322
- emit({ type: "assistant-message", text: partial.visible });
3363
+ else if (!writesDirectly) {
3364
+ emit({ type: "assistant-message", text: "" });
3323
3365
  }
3324
3366
  if (partial.hasThinking && !hasShownToolCall) {
3325
3367
  writeThinkingBlock(partial.thinkContent);
@@ -3412,11 +3454,16 @@ export async function runAgentTurn(prompt, options = {}) {
3412
3454
  const usedNativeProtocol = Boolean(completion.toolCalls?.length) ||
3413
3455
  (toolsAttached && !isTextOnlyModel(provider, model));
3414
3456
  const assistantTextResult = rememberThinkingFromText(completion.text);
3415
- assistantText = assistantTextResult;
3457
+ const continuedVisible = trimExactContinuationOverlap(interruptedVisible, assistantTextResult.visible);
3458
+ canonicalAssistantVisible = interruptedVisible + continuedVisible;
3459
+ assistantText = {
3460
+ ...assistantTextResult,
3461
+ visible: continuedVisible,
3462
+ };
3416
3463
  const commitAssistantRetry = (historyText) => {
3417
3464
  const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
3418
3465
  if (!hasShownToolCall) {
3419
- const displayText = textBeforeToolCall(stripThinking(collapseRepeatedText(completion.text)).visible).trim();
3466
+ const displayText = textBeforeToolCall(collapseRepeatedText(assistantText.visible)).trim();
3420
3467
  if (displayText) {
3421
3468
  writeAssistantMessage(displayText);
3422
3469
  }
@@ -3431,6 +3478,7 @@ export async function runAgentTurn(prompt, options = {}) {
3431
3478
  }
3432
3479
  }
3433
3480
  pushAssistantHistory(historyText);
3481
+ interruptedVisible = "";
3434
3482
  };
3435
3483
  // Only emit a thinking-block event when the classic renderer is
3436
3484
  // active (writesDirectly / no deltaParser). In TUI v2 the
@@ -3600,7 +3648,7 @@ export async function runAgentTurn(prompt, options = {}) {
3600
3648
  }
3601
3649
  }
3602
3650
  }
3603
- if (!assistantText.visible.trim() && !call) {
3651
+ if (!canonicalAssistantVisible.trim() && !call) {
3604
3652
  emptyVisibleRetries += 1;
3605
3653
  if (emptyVisibleRetries <= 3) {
3606
3654
  if (assistantText.hasThinking) {
@@ -3611,7 +3659,7 @@ export async function runAgentTurn(prompt, options = {}) {
3611
3659
  }
3612
3660
  if (assistantText.hasThinking)
3613
3661
  retryWithoutThinking = true;
3614
- commitAssistantRetry(stripThinking(collapseRepeatedText(completion.text)).visible);
3662
+ commitAssistantRetry(assistantText.visible);
3615
3663
  // Keep nudges SHORT — cheap models lose the key instruction in long text.
3616
3664
  const buildNudge = freshWebSearchRequired && !sawFreshWebSearch
3617
3665
  ? toolsAttached
@@ -3817,7 +3865,8 @@ export async function runAgentTurn(prompt, options = {}) {
3817
3865
  }
3818
3866
  // Exhausted retries — fall through to the normal path.
3819
3867
  }
3820
- const cleaned = collapseRepeatedText(stripSentinelTokens(assistantText.visible));
3868
+ const displayCleaned = collapseRepeatedText(stripSentinelTokens(assistantText.visible));
3869
+ const cleaned = collapseRepeatedText(stripSentinelTokens(canonicalAssistantVisible));
3821
3870
  const narratedAction = looksLikeActionNarration(cleaned);
3822
3871
  const narratedWebAction = looksLikeWebActionNarration(cleaned);
3823
3872
  const reconciledPlanAtCompletion = await reconcileOpenTaskBeforeFinalizing();
@@ -4089,13 +4138,12 @@ export async function runAgentTurn(prompt, options = {}) {
4089
4138
  outcomeStatus,
4090
4139
  remainingCriteria,
4091
4140
  });
4092
- writeAssistantMessage(cleaned);
4093
4141
  lastAnswer = cleaned;
4094
4142
  return finishTurn(lastAnswer, step + 1, outcomeStatus, remainingCriteria, outcomeStatus === "failed"
4095
4143
  ? "One or more required plan tasks failed."
4096
4144
  : outcomeStatus === "partial"
4097
4145
  ? "Required outcome criteria remain unsupported by current evidence."
4098
- : undefined);
4146
+ : undefined, displayCleaned);
4099
4147
  }
4100
4148
  // A valid primary tool call exists for this fresh model turn. Show any
4101
4149
  // prose / thinking that preceded it, record the assistant message ONCE.
@@ -4104,10 +4152,13 @@ export async function runAgentTurn(prompt, options = {}) {
4104
4152
  : nativeToolCalls.length
4105
4153
  ? assistantText.visible.trim()
4106
4154
  : textBeforeToolCall(assistantText.visible);
4107
- if (beforeTool &&
4108
- !deferredToolCalls.some((entry) => entry.shown)) {
4155
+ if (beforeTool) {
4109
4156
  writeAssistantMessage(beforeTool);
4110
4157
  }
4158
+ else if (deltaParser) {
4159
+ emit({ type: "assistant-message", text: "" });
4160
+ }
4161
+ interruptedVisible = "";
4111
4162
  let bound = [];
4112
4163
  if (nativeToolCalls.length) {
4113
4164
  bound = nativeToolCalls.map((tc, index) => {