koneck 2.95.0 → 2.96.0

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/engine.js CHANGED
@@ -7,7 +7,7 @@ import { execa } from 'execa';
7
7
  import { TOOL_DEFINITIONS, dispatchTool, describeShell, unknownToolError, resolveToolAlias, aliasNotice, isToolFailure, } from './tools.js';
8
8
  import { tui } from './tui.js';
9
9
  import { hasWorkspaceOwnedGitRepo, gitScope, createWorktree, mergeWorktree, preservedWorktreeNotice } from './worktree.js';
10
- import { resolveProvider, getApiKey, keyProvenance, servesLocally, servesOwnWeights, needsNoCredential, isPrivateHost } from './providers.js';
10
+ import { resolveProvider, getApiKey, keyProvenance, servesLocally, servesOwnWeights, needsNoCredential } from './providers.js';
11
11
  import { wireEffort, looksLikeEffortUnsupported, acceptedEfforts, nearestAccepted } from './pace.js';
12
12
  import { triggerFor, pickNext, describeSwitch, askSwitch, keyOf as failoverKey } from './failover.js';
13
13
  import { findChurn, describeChurn, WINDOW as CHURN_WINDOW } from './cycle.js';
@@ -17,7 +17,7 @@ import { handoffFrom, describeHandoff, worthSaying } from './handoff.js';
17
17
  import { readAll as readLedger, relevant as relevantFindings, withStaleness, describeFindings, deriveFindings, record as recordFindings, changedSince, currentSha } from './ledger.js';
18
18
  import { repoNameFor } from './session-bus.js';
19
19
  import { compressIfNeeded } from './compressor.js';
20
- import { contextLimitFor, looksLikeContextOverflow, parseContextLimit, fetchModelContextLimit, ollamaServingLimit, fixedRequestTokens, overheadIsCrowded, crowdedWindowAdvice, probeOllamaContext, detectWindowPlateau, truncationNotice, } from './context-limit.js';
20
+ import { contextLimitFor, looksLikeContextOverflow, parseContextLimit, fetchModelContextLimit, ollamaServingLimit, fixedRequestTokens, overheadIsCrowded, crowdedWindowAdvice, probeOllamaContext, detectWindowPlateau, truncationNotice, ollamaWindowFix, runsOnThisMachine, } from './context-limit.js';
21
21
  import { generateSessionId, saveSession } from './session.js';
22
22
  import { EMPTY_PLAN, parsePlan, planReminder, planAck, noPlanReminder } from './plan.js';
23
23
  import { spillIfOversized } from './spill.js';
@@ -26,6 +26,7 @@ import { resolveSandbox, describeSandbox } from './sandbox.js';
26
26
  import { loadImages, buildUserContent, imageMime, looksLikeVisionUnsupported, visionUnsupportedNotice, } from './images.js';
27
27
  import { parseTextToolCalls, paramTypesFrom, looksLikeTextToolCall, stripTemplateTokens, splitEmittable, parseCommandStyleCalls, toolShapesFrom } from './tool-call-text.js';
28
28
  import { isIdentityQuestion, identityAnswer } from './identity.js';
29
+ import { deniedCapabilities, capabilityCorrection, capabilityNotice } from './capability-claims.js';
29
30
  import { leanTools, leanModeNotice, findToolsSchema, matchDeferred, deferredToolNotice, FIND_TOOLS } from './lean-mode.js';
30
31
  import { findTailRepetition, trimTailRepetition, repetitionNotice, CHECK_EVERY } from './repetition.js';
31
32
  import { loadMemory } from './memory.js';
@@ -1100,7 +1101,17 @@ export function toolDetailFor(name, argsJson) {
1100
1101
  * before a conversation starts, so on the default there is almost no room to work in, and the
1101
1102
  * symptom is exactly this: nothing comes back.
1102
1103
  */
1103
- export function emptyResponseAdvice(provider, model, count, toolsWorked, endpoint) {
1104
+ export function emptyResponseAdvice(provider, model, count, toolsWorked, endpoint,
1105
+ /*
1106
+ * What this run's instructions and tool descriptions actually cost.
1107
+ *
1108
+ * This was the string "about 3.8k", with a comment four lines above it saying 3,600 — two
1109
+ * different figures for the same thing in the same file, neither of them measured. The real
1110
+ * number moves with the tools a run has: lean mode drops some, an MCP server adds more, and a
1111
+ * session with skills loaded is larger again. Quoting a constant is asserting something not
1112
+ * known, and it is the number somebody uses to decide what to set the window to.
1113
+ */
1114
+ fixedTokens, platform) {
1104
1115
  // The endpoint is named, not just the provider slot. Reported as "omni returned 3 empty
1105
1116
  // responses ... yet i am using ollama" — both true at once, because the endpoint saved under the
1106
1117
  // omni slot is an Ollama box. Naming the address answers the objection in the message itself.
@@ -1123,13 +1134,19 @@ export function emptyResponseAdvice(provider, model, count, toolsWorked, endpoin
1123
1134
  */
1124
1135
  const runtime = servesOwnWeights(provider);
1125
1136
  const ownNetwork = endpoint ? servesLocally(provider, endpoint) : provider.toLowerCase() === 'ollama';
1137
+ const remote = endpoint ? !runsOnThisMachine(endpoint) : false;
1138
+ const cost = fixedTokens && fixedTokens > 0
1139
+ ? `${fixedTokens.toLocaleString()} tokens of that — measured for this run, with the tools it `
1140
+ + `actually has —`
1141
+ : 'most of that';
1126
1142
  const ollamaFix = 'Ollama serves a default of 4,096 tokens whatever the model was trained for, and silently ' +
1127
1143
  'truncates a longer prompt instead of refusing it — and KONECK\'s instructions and tool ' +
1128
- 'descriptions come to about 3.8k of that before your conversation is counted, which is exactly ' +
1129
- 'how a turn comes back with nothing in it. Set OLLAMA_CONTEXT_LENGTH=32768 where Ollama runs' +
1130
- (endpoint ? ` the machine serving ${endpoint}` : '') +
1131
- ', or use a Modelfile with PARAMETER num_ctx 32768, and restart it. If you know the real ' +
1132
- 'limit, `/config contextTokens <n>` makes KONECK compact before reaching it.';
1144
+ `descriptions come to ${cost} before your conversation is counted, which is exactly ` +
1145
+ 'how a turn comes back with nothing in it.\n\n' +
1146
+ ollamaWindowFix({ ...(endpoint !== undefined ? { endpoint } : {}), remote,
1147
+ ...(platform !== undefined ? { platform } : {}) }) +
1148
+ '\n\nIf you know the real limit, `/config contextTokens <n>` makes KONECK compact before ' +
1149
+ 'reaching it.';
1133
1150
  if (runtime) {
1134
1151
  return observed + '\nThe likeliest cause is the context window rather than the model. ' + ollamaFix;
1135
1152
  }
@@ -1485,12 +1502,16 @@ detectedContext = { current: 0 }) {
1485
1502
  let textToolsNoticed = false;
1486
1503
  /** Said once: a model that writes calls as shell commands will keep doing it. */
1487
1504
  let commandStyleNoticed = false;
1505
+ /** One correction per user message: a model that repeats a false denial will not be argued out. */
1506
+ let capabilityCorrected = false;
1488
1507
  // The tool list, cut to fit when the window cannot hold it.
1489
1508
  //
1490
1509
  // Nine of the twenty-three tools are git subcommands, about 550 tokens between them, and every
1491
1510
  // one is a line of shell that execute_command already runs. Reported as a session that planned
1492
1511
  // three steps, called `plan`, and then returned three empty turns: there was nothing left of the
1493
1512
  // window to answer in.
1513
+ /** What this run costs before the conversation is counted. Set just below, read at the plateau. */
1514
+ let fixedCost = 0;
1494
1515
  // What every request costs before the user has typed anything — the instructions plus the tool
1495
1516
  // descriptions. On a large window this is 12% of it and there is nothing to discuss. On a small
1496
1517
  // one it was 93%, which is why a model looped, wrote tool calls as prose, and answered questions
@@ -1518,6 +1539,14 @@ detectedContext = { current: 0 }) {
1518
1539
  fixed = fixedRequestTokens(systemChars, JSON.stringify(openaiTools));
1519
1540
  }
1520
1541
  }
1542
+ /*
1543
+ * Recorded after lean mode has had its go, so it is the floor that actually applies.
1544
+ *
1545
+ * A window discovered mid-stream is compared against this. Before lean mode it would overstate
1546
+ * the floor, and telling somebody their window cannot hold a prompt that has since been
1547
+ * trimmed to fit would be the wrong answer confidently given.
1548
+ */
1549
+ fixedCost = fixed;
1521
1550
  // Said only if it still does not fit after that, because otherwise it is solved rather than
1522
1551
  // worth a warning.
1523
1552
  if (overheadIsCrowded(fixed, effectiveLimit())) {
@@ -1527,9 +1556,8 @@ detectedContext = { current: 0 }) {
1527
1556
  // OLLAMA_CONTEXT_LENGTH is read by the server, so for an endpoint on another machine it
1528
1557
  // has to be set there. The advice said "in Ollama's environment", which for a remote box
1529
1558
  // sends someone to restart the wrong one.
1530
- remote: !isPrivateHost(resolveProvider(config.provider, config.baseURL).baseURL)
1531
- ? false
1532
- : !/localhost|127\.0\.0\.1|::1/.test(resolveProvider(config.provider, config.baseURL).baseURL),
1559
+ // Loopback means this machine; a LAN address is somebody else's. See runsOnThisMachine.
1560
+ remote: !runsOnThisMachine(resolveProvider(config.provider, config.baseURL).baseURL),
1533
1561
  });
1534
1562
  config.onCrowdedWindow?.(fixed, effectiveLimit(), advice);
1535
1563
  if (!silent)
@@ -1861,9 +1889,17 @@ detectedContext = { current: 0 }) {
1861
1889
  truncationSeen = true;
1862
1890
  contextLimit = plateau.window;
1863
1891
  config.contextTokens = plateau.window; // so compaction plans around the real figure
1864
- config.onTruncation?.(plateau.window, truncationNotice(plateau.window));
1892
+ const endpoint = resolveProvider(config.provider, config.baseURL).baseURL;
1893
+ const said = truncationNotice(plateau.window, {
1894
+ floor: fixedCost,
1895
+ endpoint,
1896
+ // Whether the thing to reconfigure is this machine or another one. KONECK knows its
1897
+ // own platform and nothing about a remote box, so a remote answer names no commands.
1898
+ remote: !runsOnThisMachine(endpoint),
1899
+ });
1900
+ config.onTruncation?.(plateau.window, said);
1865
1901
  if (!silent)
1866
- tui.printWarning(truncationNotice(plateau.window));
1902
+ tui.printWarning(said);
1867
1903
  }
1868
1904
  }
1869
1905
  }
@@ -2341,6 +2377,36 @@ detectedContext = { current: 0 }) {
2341
2377
  }
2342
2378
  }
2343
2379
  }
2380
+ /*
2381
+ * A reply that denies a capability this run has.
2382
+ *
2383
+ * "I cannot visit websites or perform login actions" — with the browser tool enabled, whose
2384
+ * description says it opens a url, fills form fields and clicks. The user's answer to that was
2385
+ * "but there's a built in browser right here", and they were right.
2386
+ *
2387
+ * Checked only after every recovery above has failed to find a call, because a model that wrote
2388
+ * a call as prose has not denied anything. And only for tools actually passed this turn: lean
2389
+ * mode defers tools and a machine may have no browser, and in those cases the denial is true.
2390
+ *
2391
+ * Once per user message. The correction goes to the model and the turn is taken again, because
2392
+ * being told the agent cannot help when it can is the worst outcome available — but a model that
2393
+ * says it a second time is not going to be argued into it, and the reader is better served by
2394
+ * the note than by a third attempt.
2395
+ */
2396
+ if (toolCalls.length === 0 && assistantContent && !capabilityCorrected) {
2397
+ const denied = deniedCapabilities(assistantContent, openaiTools.map(t => t.function.name));
2398
+ if (denied.length > 0) {
2399
+ capabilityCorrected = true;
2400
+ const notice = capabilityNotice(denied, config.model);
2401
+ config.onFalseDenial?.(denied.map(d => d.what), notice);
2402
+ if (!silent)
2403
+ tui.printWarning(notice);
2404
+ messages.push({ role: 'assistant', content: assistantContent });
2405
+ messages.push({ role: 'system', content: capabilityCorrection(denied) });
2406
+ assistantContent = '';
2407
+ continue;
2408
+ }
2409
+ }
2344
2410
  // Only print buffered content if it wasn't already live-streamed
2345
2411
  if (!silent && assistantContent.trim() && !tui.hadStreamOutput())
2346
2412
  tui.printAssistant(assistantContent);
@@ -2449,7 +2515,9 @@ detectedContext = { current: 0 }) {
2449
2515
  // tools — and it said the model was not usable, about a model that had just called
2450
2516
  // `plan` successfully one turn earlier. Neither helped, and both pointed away from the
2451
2517
  // likely cause.
2452
- throw new Error(emptyResponseAdvice(config.provider, config.model, emptyTurns, toolsProvenToWork, resolveProvider(config.provider, config.baseURL).baseURL));
2518
+ throw new Error(emptyResponseAdvice(config.provider, config.model, emptyTurns, toolsProvenToWork, resolveProvider(config.provider, config.baseURL).baseURL,
2519
+ // The floor this run actually has, rather than a constant in a string.
2520
+ fixedCost));
2453
2521
  }
2454
2522
  }
2455
2523
  else {