@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.
Files changed (51) hide show
  1. package/dist/agent/runner.js +122 -29
  2. package/dist/agent/runner.js.map +1 -1
  3. package/dist/agent/stream-recovery.d.ts +15 -1
  4. package/dist/agent/stream-recovery.js +42 -2
  5. package/dist/agent/stream-recovery.js.map +1 -1
  6. package/dist/llm/agentrouter.js +2 -2
  7. package/dist/llm/agentrouter.js.map +1 -1
  8. package/dist/llm/bynara.js +2 -2
  9. package/dist/llm/bynara.js.map +1 -1
  10. package/dist/llm/http.d.ts +29 -3
  11. package/dist/llm/http.js +110 -25
  12. package/dist/llm/http.js.map +1 -1
  13. package/dist/llm/modal.js +7 -2
  14. package/dist/llm/modal.js.map +1 -1
  15. package/dist/llm/nvidia.js +6 -6
  16. package/dist/llm/nvidia.js.map +1 -1
  17. package/dist/llm/router.d.ts +1 -1
  18. package/dist/llm/router.js +59 -15
  19. package/dist/llm/router.js.map +1 -1
  20. package/dist/modes/ask.js +91 -23
  21. package/dist/modes/ask.js.map +1 -1
  22. package/dist/prompts/embedded.js +2 -2
  23. package/dist/prompts/embedded.js.map +1 -1
  24. package/dist/prompts/index.d.ts +7 -0
  25. package/dist/prompts/index.js +44 -4
  26. package/dist/prompts/index.js.map +1 -1
  27. package/dist/prompts/system.agent.md +2 -1
  28. package/dist/prompts/system.ask.md +1 -0
  29. package/dist/safety/classifier.js +3 -0
  30. package/dist/safety/classifier.js.map +1 -1
  31. package/dist/tools/definitions.js +15 -0
  32. package/dist/tools/definitions.js.map +1 -1
  33. package/dist/tools/image.d.ts +19 -1
  34. package/dist/tools/image.js +149 -1
  35. package/dist/tools/image.js.map +1 -1
  36. package/dist/tools/registry.js +13 -1
  37. package/dist/tools/registry.js.map +1 -1
  38. package/dist/tools/tool-types.d.ts +8 -1
  39. package/dist/tui-v2/components/transcript/thinking-block.js +6 -7
  40. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
  41. package/dist/tui-v2/components/transcript/transcript-view.js +155 -28
  42. package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -1
  43. package/dist/tui-v2/components/transcript/use-transcript-selection.d.ts +2 -2
  44. package/dist/tui-v2/components/transcript/use-transcript-selection.js +7 -8
  45. package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -1
  46. package/dist/tui-v2/state/transcript-reducer.js +7 -12
  47. package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
  48. package/dist/types.d.ts +17 -0
  49. package/dist/version.generated.d.ts +2 -2
  50. package/dist/version.generated.js +2 -2
  51. package/package.json +1 -1
@@ -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 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.
@@ -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
- // A vision-capable request already carries the actual image bytes. Hiding
374
- // image.ocr from the model prevents it from replacing visual inspection
375
- // with a lossy Tesseract pass (which produced fabricated screenshot text).
376
- const toolNames = availableToolNames().filter((name) => name !== "image.ocr" || imageOcrEnabled);
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 = options.provider ?? config.defaultProvider;
455
+ let provider = initialProvider;
406
456
  await ensureProviderConfigured(provider);
407
- let model = options.model ?? config.defaultModel;
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([...toolNames, ...RUNNER_META_TOOL_NAMES]);
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)(toolNames.join(", "), {
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 partialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible).trim();
3377
+ const rawPartialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible);
3378
+ const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
3379
+ const partialVisible = normalizedPartialVisible.trim();
3315
3380
  if (partialVisible) {
3316
- if (!hasShownToolCall) {
3317
- writeAssistantMessage(partialVisible);
3318
- }
3381
+ writeAssistantMessage(partialVisible);
3319
3382
  pushAssistantHistory(partialVisible);
3383
+ interruptedVisible += normalizedPartialVisible;
3320
3384
  }
3321
- else if (!writesDirectly && !hasShownToolCall) {
3322
- emit({ type: "assistant-message", text: partial.visible });
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
- assistantText = assistantTextResult;
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(stripThinking(collapseRepeatedText(completion.text)).visible).trim();
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 (!assistantText.visible.trim() && !call) {
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(stripThinking(collapseRepeatedText(completion.text)).visible);
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 cleaned = collapseRepeatedText(stripSentinelTokens(assistantText.visible));
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;