clixad 0.0.1-beta.48 → 0.0.1-beta.49

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.
@@ -468,6 +468,13 @@ var MaintenanceError = class extends Error {
468
468
  }
469
469
  };
470
470
  var MAINTENANCE = "maintenance";
471
+ var PromptTooLongError = class extends Error {
472
+ constructor(message) {
473
+ super(message);
474
+ this.name = "PromptTooLongError";
475
+ }
476
+ };
477
+ var PROMPT_TOO_LONG = "prompt_too_long";
471
478
  async function refusal503(res, what) {
472
479
  const text = await res.text().catch(() => "");
473
480
  try {
@@ -537,7 +544,7 @@ async function refusal400(res, what) {
537
544
  const text = await res.text().catch(() => "");
538
545
  try {
539
546
  const body = JSON.parse(text);
540
- if (body.error === "prompt_too_long" && typeof body.message === "string") return new Error(body.message);
547
+ if (body.error === PROMPT_TOO_LONG && typeof body.message === "string") return new PromptTooLongError(body.message);
541
548
  } catch {
542
549
  }
543
550
  const detail = safeDetail(text);
@@ -1482,6 +1489,7 @@ var GatewayClient = class {
1482
1489
  }
1483
1490
  if (premiumRefusal) throw premiumRefusal;
1484
1491
  if (streamError && streamErrorType === MAINTENANCE) throw new MaintenanceError(streamError);
1492
+ if (streamError && streamErrorType === PROMPT_TOO_LONG) throw new PromptTooLongError(streamError);
1485
1493
  if (streamError && streamErrorType && LIMIT_REFUSALS.has(streamErrorType)) {
1486
1494
  throw new LimitRefusalError(streamError, streamErrorType, spendable ? { figures: spendable, balance } : void 0);
1487
1495
  }
@@ -6830,6 +6838,24 @@ var COMPACT_THRESHOLD = 0.75;
6830
6838
  function shouldCompact(messages, contextTokens, threshold = COMPACT_THRESHOLD) {
6831
6839
  return conversationTokens(messages) > contextTokens * threshold;
6832
6840
  }
6841
+ function compactBudget(contextTokens) {
6842
+ return Math.floor(contextTokens * COMPACT_THRESHOLD);
6843
+ }
6844
+ function messageWeight(message) {
6845
+ let images = 0;
6846
+ const content = Array.isArray(message.content) ? message.content.map((part) => {
6847
+ if (part.type !== "image_url") return part;
6848
+ images++;
6849
+ return { ...part, image_url: { url: "" } };
6850
+ }) : message.content;
6851
+ const bytes = Buffer.byteLength(JSON.stringify({ ...message, content }));
6852
+ return Math.ceil(bytes / 4) + images * IMAGE_TOKEN_ESTIMATE;
6853
+ }
6854
+ function requestTokens(messages) {
6855
+ let total = 0;
6856
+ for (const m of messages) total += messageWeight(m);
6857
+ return total;
6858
+ }
6833
6859
  function cutPoint(messages, keepTurns = 2) {
6834
6860
  let seen = 0;
6835
6861
  for (let i = messages.length - 1; i > 0; i--) {
@@ -6839,37 +6865,187 @@ function cutPoint(messages, keepTurns = 2) {
6839
6865
  }
6840
6866
  return 0;
6841
6867
  }
6868
+ function safeCuts(messages) {
6869
+ const open = /* @__PURE__ */ new Set();
6870
+ const cuts = [];
6871
+ for (let i = 1; i <= messages.length; i++) {
6872
+ const prev = messages[i - 1];
6873
+ if (prev.role === "assistant") for (const call of prev.tool_calls ?? []) open.add(call.id);
6874
+ if (prev.role === "tool" && prev.tool_call_id) open.delete(prev.tool_call_id);
6875
+ if (open.size === 0 && (i === messages.length || messages[i].role !== "tool")) cuts.push(i);
6876
+ }
6877
+ return cuts;
6878
+ }
6842
6879
  var SUMMARY_LEAD = "Summary of the earlier conversation:\n\n";
6843
6880
  var SUMMARY_ACK = "Understood \u2014 continuing from that summary.";
6844
- var SUMMARY_PROMPT = "Summarise the conversation above for your future self, which will continue the work without seeing it. Cover: what the user asked for, what was already changed (files and the reasoning), what was learned about the codebase, and what is still open. Be specific about file paths and names. No preamble.";
6845
- async function compact(client, model, messages, opts = {}) {
6846
- const cut2 = cutPoint(messages, opts.keepTurns ?? 2);
6847
- if (cut2 <= 1) return void 0;
6881
+ function withSummary(summary, rest) {
6882
+ const lead = { role: "user", content: `${SUMMARY_LEAD}${summary}` };
6883
+ return rest[0]?.role === "assistant" ? [lead, ...rest] : [lead, { role: "assistant", content: SUMMARY_ACK }, ...rest];
6884
+ }
6885
+ function summaryReserve(budget) {
6886
+ return Math.min(1e4, Math.max(1e3, Math.floor(budget / 6)));
6887
+ }
6888
+ function summaryPrompt(words) {
6889
+ return `Summarise the conversation above for your future self, which will continue the work without seeing it. Cover: what the user asked for, what was already changed (files and the reasoning), what was learned about the codebase, and what is still open. Be specific about file paths and names. If it opens with a summary of earlier messages, carry what that summary says forward. At most ${words.toLocaleString("en-US")} words. No preamble.`;
6890
+ }
6891
+ function planCompaction(messages, contextTokens, keepTurns = 2) {
6892
+ if (messages.length < 2) return void 0;
6893
+ const budget = compactBudget(contextTokens);
6894
+ const suffix = new Array(messages.length + 1).fill(0);
6895
+ for (let i = messages.length - 1; i >= 0; i--) suffix[i] = suffix[i + 1] + messageWeight(messages[i]);
6896
+ const total = suffix[0];
6897
+ const tail = (from) => suffix[from];
6898
+ const fixed = total - suffix[1] + summaryReserve(budget) + messageWeight({ role: "assistant", content: SUMMARY_ACK });
6899
+ const preferred = cutPoint(messages, keepTurns);
6900
+ const over = total > budget;
6901
+ if (!over) return preferred > 1 ? { cut: preferred, fits: fixed + tail(preferred) <= budget, over } : void 0;
6902
+ const cuts = safeCuts(messages).filter((c) => c >= Math.max(preferred, 2));
6903
+ if (cuts.length === 0) return void 0;
6904
+ const keep = Math.min(budget - fixed, Math.floor(budget / 2));
6905
+ const cut2 = cuts.find((c) => tail(c) <= keep) ?? cuts.at(-1);
6906
+ return { cut: cut2, fits: fixed + tail(cut2) <= budget, over };
6907
+ }
6908
+ function cutNote(left) {
6909
+ return `
6910
+ \u2026 [${left.toLocaleString("en-US")} more characters left out here]`;
6911
+ }
6912
+ function capText(text, limit) {
6913
+ return text.length > limit ? `${text.slice(0, limit)}${cutNote(text.length - limit)}` : text;
6914
+ }
6915
+ function capArguments(json, limit) {
6916
+ const walk2 = (v) => {
6917
+ if (typeof v === "string") return capText(v, limit);
6918
+ if (Array.isArray(v)) return v.map(walk2);
6919
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk2(x)]));
6920
+ return v;
6921
+ };
6922
+ try {
6923
+ return JSON.stringify(walk2(JSON.parse(json)));
6924
+ } catch {
6925
+ return json.length > limit ? JSON.stringify({ arguments: capText(json, limit) }) : json;
6926
+ }
6927
+ }
6928
+ function capMessage(message, limit) {
6929
+ const content = typeof message.content === "string" ? capText(message.content, limit) : Array.isArray(message.content) ? message.content.map((part) => part.type === "text" ? { ...part, text: capText(part.text, limit) } : part) : message.content;
6930
+ const out = { ...message, content };
6931
+ if (message.tool_calls) {
6932
+ out.tool_calls = message.tool_calls.map((c) => ({
6933
+ ...c,
6934
+ function: { ...c.function, arguments: capArguments(c.function.arguments, limit) }
6935
+ }));
6936
+ }
6937
+ return out;
6938
+ }
6939
+ function withoutImages2(message) {
6940
+ if (!Array.isArray(message.content)) return message;
6941
+ return {
6942
+ ...message,
6943
+ content: message.content.map(
6944
+ (part) => part.type === "image_url" ? { type: "text", text: "[an image, not shown here]" } : part
6945
+ )
6946
+ };
6947
+ }
6948
+ function longestText(messages) {
6949
+ let longest = 0;
6950
+ for (const m of messages) {
6951
+ longest = Math.max(longest, messageText(m).length);
6952
+ for (const c of m.tool_calls ?? []) longest = Math.max(longest, c.function.arguments.length);
6953
+ }
6954
+ return longest;
6955
+ }
6956
+ function shrinkToFit(messages, room) {
6957
+ if (requestTokens(messages) <= room) return messages;
6958
+ const plain2 = messages.map(withoutImages2);
6959
+ if (requestTokens(plain2) <= room) return plain2;
6960
+ const capped = (limit) => plain2.map((m) => capMessage(m, limit));
6961
+ let lo = 0;
6962
+ let hi = longestText(plain2);
6963
+ while (lo < hi) {
6964
+ const mid = Math.ceil((lo + hi) / 2);
6965
+ if (requestTokens(capped(mid)) <= room) lo = mid;
6966
+ else hi = mid - 1;
6967
+ }
6968
+ return capped(lo);
6969
+ }
6970
+ async function compact(client, model, messages, contextTokens, opts = {}) {
6971
+ const plan = planCompaction(messages, contextTokens, opts.keepTurns ?? 2);
6972
+ if (!plan) return void 0;
6973
+ const { cut: cut2 } = plan;
6974
+ const budget = compactBudget(contextTokens);
6975
+ const system = messages[0];
6976
+ const prompt = { role: "user", content: summaryPrompt(Math.floor(summaryReserve(budget) / 2)) };
6977
+ const bounds = [1, ...safeCuts(messages).filter((c) => c > 1 && c < cut2), cut2];
6978
+ const rounds = [];
6979
+ for (let i = 0; i + 1 < bounds.length; i++) rounds.push(messages.slice(bounds[i], bounds[i + 1]));
6848
6980
  const before = conversationTokens(messages);
6849
- const turn = await client.chatStream(
6850
- [...messages.slice(0, cut2), { role: "user", content: SUMMARY_PROMPT }],
6851
- model,
6852
- // The `/effort` setting applies to every call on the model, a summary
6853
- // included: somebody who turned it down to save credits saves them here too.
6854
- { signal: opts.signal, effort: opts.effort }
6855
- );
6856
- const summary = messageText(turn.message).trim() || "(no summary returned)";
6857
- const compacted = [
6858
- messages[0],
6859
- { role: "user", content: `${SUMMARY_LEAD}${summary}` },
6860
- { role: "assistant", content: SUMMARY_ACK },
6861
- ...messages.slice(cut2)
6862
- ];
6981
+ let summary;
6982
+ let creditsCharged = 0;
6983
+ let balance = 0;
6984
+ let spendable;
6985
+ let calls = 0;
6986
+ let next = 0;
6987
+ while (next < rounds.length) {
6988
+ const room = Math.max(
6989
+ 1e3,
6990
+ budget - requestTokens([system, prompt]) - (summary === void 0 ? 0 : requestTokens(withSummary(summary, [])))
6991
+ );
6992
+ let chunk = shrinkToFit(rounds[next++], room);
6993
+ while (next < rounds.length) {
6994
+ const round = rounds[next];
6995
+ const left = room - requestTokens(chunk);
6996
+ if (requestTokens(round) <= left) {
6997
+ chunk = [...chunk, ...round];
6998
+ next++;
6999
+ continue;
7000
+ }
7001
+ if (requestTokens(round) > room && left >= room / 2) {
7002
+ chunk = [...chunk, ...shrinkToFit(round, left)];
7003
+ next++;
7004
+ }
7005
+ break;
7006
+ }
7007
+ const request = [system, ...summary === void 0 ? chunk : withSummary(summary, chunk), prompt];
7008
+ let text = "";
7009
+ for (let attempt = 0; attempt < 2 && !text; attempt++) {
7010
+ const turn = await client.chatStream(request, model, { signal: opts.signal, effort: opts.effort });
7011
+ calls++;
7012
+ creditsCharged += turn.creditsCharged;
7013
+ balance = turn.balance;
7014
+ spendable = turn.spendable;
7015
+ text = messageText(turn.message).trim();
7016
+ }
7017
+ if (!text) throw new Error("the model returned an empty summary, so the conversation was left as it was");
7018
+ summary = text;
7019
+ }
7020
+ const tailMessages = messages.slice(cut2);
7021
+ let final = summary ?? "";
7022
+ const excess = requestTokens([system, ...withSummary(final, tailMessages)]) - budget;
7023
+ if (excess > 0) {
7024
+ const lead = { role: "user", content: final };
7025
+ final = messageText(shrinkToFit([lead], Math.max(0, messageWeight(lead) - excess - 1))[0]);
7026
+ }
7027
+ const compacted = [system, ...withSummary(final, tailMessages)];
6863
7028
  return {
6864
7029
  messages: compacted,
6865
- summary,
6866
- creditsCharged: turn.creditsCharged,
6867
- balance: turn.balance,
6868
- ...turn.spendable ? { spendable: turn.spendable } : {},
7030
+ summary: final,
7031
+ creditsCharged,
7032
+ balance,
7033
+ ...spendable ? { spendable } : {},
6869
7034
  before,
6870
- after: conversationTokens(compacted)
7035
+ after: conversationTokens(compacted),
7036
+ calls
6871
7037
  };
6872
7038
  }
7039
+ var COMPACT_ADVICE = "The work done so far is kept. Run /compact to summarise the conversation, this task's steps included, then send a message to carry on. /clear starts over, and /model switches to a model that takes longer conversations.";
7040
+ function withCompactAdvice(refusal) {
7041
+ const lines = refusal.split("\n");
7042
+ if (lines.length === 3) return [lines[0], COMPACT_ADVICE, lines[2]].join("\n");
7043
+ return [...lines, COMPACT_ADVICE].join("\n");
7044
+ }
7045
+ function compactionHelps(messages, contextTokens) {
7046
+ const plan = planCompaction(messages, contextTokens);
7047
+ return plan !== void 0 && plan.over && plan.fits;
7048
+ }
6873
7049
 
6874
7050
  // src/version.ts
6875
7051
  import { readFileSync as readFileSync6 } from "node:fs";
@@ -6979,6 +7155,7 @@ export {
6979
7155
  PremiumLockedError,
6980
7156
  LimitRefusalError,
6981
7157
  MaintenanceError,
7158
+ PromptTooLongError,
6982
7159
  shortfallHeadline,
6983
7160
  AuthError,
6984
7161
  GatewayClient,
@@ -7063,6 +7240,8 @@ export {
7063
7240
  SUMMARY_LEAD,
7064
7241
  SUMMARY_ACK,
7065
7242
  compact,
7243
+ withCompactAdvice,
7244
+ compactionHelps,
7066
7245
  VERSION,
7067
7246
  VERSION_LABEL,
7068
7247
  isNewerVersion,
package/dist/clixad.mjs CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  takeConfigNotices,
55
55
  toolSchemaFor,
56
56
  usd
57
- } from "./chunk-CNLFGTYS.mjs";
57
+ } from "./chunk-E7SAKAVQ.mjs";
58
58
 
59
59
  // src/tui/load.ts
60
60
  import * as nodeModule from "node:module";
@@ -133,7 +133,7 @@ async function withProductionReact(load) {
133
133
  }
134
134
  function loadTui() {
135
135
  boundInkCaches();
136
- return withProductionReact(() => import("./tui-M5SJRCHM.mjs"));
136
+ return withProductionReact(() => import("./tui-2H2XRALC.mjs"));
137
137
  }
138
138
 
139
139
  // src/title.ts
@@ -18,6 +18,7 @@ import {
18
18
  NO_SEARCH_INSTRUCTION,
19
19
  PaywallError,
20
20
  PremiumLockedError,
21
+ PromptTooLongError,
21
22
  SPINNER_TICKS_PER_FRAME,
22
23
  SUMMARY_ACK,
23
24
  SUMMARY_LEAD,
@@ -35,6 +36,7 @@ import {
35
36
  colorEnabled,
36
37
  compact,
37
38
  compactBanner,
39
+ compactionHelps,
38
40
  contextTokensFor,
39
41
  conversationTokens,
40
42
  createPermit,
@@ -104,8 +106,9 @@ import {
104
106
  usd,
105
107
  visibleLength,
106
108
  webSearchCostLine,
107
- welcomeBanner
108
- } from "./chunk-CNLFGTYS.mjs";
109
+ welcomeBanner,
110
+ withCompactAdvice
111
+ } from "./chunk-E7SAKAVQ.mjs";
109
112
 
110
113
  // src/tui/index.tsx
111
114
  import "react";
@@ -2738,10 +2741,13 @@ function App({
2738
2741
  try {
2739
2742
  const window = contextWindow(model2);
2740
2743
  if (shouldCompact([{ role: "system", content: contextRef.current.systemPrompt }, ...history], window)) {
2741
- const res = await compact(client, model2, [
2742
- { role: "system", content: contextRef.current.systemPrompt },
2743
- ...history
2744
- ], { signal: ac.signal, effort: savedEffort(config, model2) });
2744
+ const res = await compact(
2745
+ client,
2746
+ model2,
2747
+ [{ role: "system", content: contextRef.current.systemPrompt }, ...history],
2748
+ window,
2749
+ { signal: ac.signal, effort: savedEffort(config, model2) }
2750
+ );
2745
2751
  if (res) {
2746
2752
  history = res.messages.slice(1);
2747
2753
  setMessages(history);
@@ -2899,6 +2905,7 @@ ${preferencesText}` : ""),
2899
2905
  setMessages(kept);
2900
2906
  persist(kept);
2901
2907
  }
2908
+ const compactAdvised = err instanceof PromptTooLongError && kept !== void 0 && compactionHelps([{ role: "system", content: contextRef.current.systemPrompt }, ...kept], contextWindow(model2));
2902
2909
  if ((err instanceof LimitRefusalError || err instanceof PremiumLockedError) && err.spendable) {
2903
2910
  balanceSeqRef.current++;
2904
2911
  takeSpendable(err.spendable.figures, err.spendable.balance);
@@ -2955,10 +2962,12 @@ ${preferencesText}` : ""),
2955
2962
  push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
2956
2963
  } else if (err instanceof MaintenanceError) {
2957
2964
  push({ kind: "notice", tone: "warn", text: ` ${errorNotice(err.message, void 0, " ")}` });
2965
+ } else if (compactAdvised) {
2966
+ push({ kind: "notice", tone: "error", text: ` error: ${errorNotice(withCompactAdvice(err.message), void 0, " ")}` });
2958
2967
  } else {
2959
2968
  push({ kind: "notice", tone: "error", text: ` error: ${errorNotice(err.message, void 0, " ")}` });
2960
2969
  }
2961
- if (kept && !(err instanceof PaywallError) && !(err instanceof PremiumLockedError)) {
2970
+ if (kept && !(err instanceof PaywallError) && !(err instanceof PremiumLockedError) && !compactAdvised) {
2962
2971
  push({ kind: "notice", text: KEPT_AFTER_ERROR });
2963
2972
  }
2964
2973
  } finally {
@@ -3399,15 +3408,17 @@ ${preferencesText}` : ""),
3399
3408
  push({ kind: "notice", text: ` mode \u2192 ${MODE_LABEL[permRef.current.mode]}` });
3400
3409
  return;
3401
3410
  }
3402
- // A model call like any other, so esc has to stop it like any other —
3403
- // and stopping it costs nothing, since the summary only replaces the
3404
- // conversation once the call has come back whole.
3411
+ // Model calls like any other, so esc has to stop them like any other.
3412
+ // Stopping leaves the conversation as it was, since the summary only
3413
+ // replaces it once every call has come back whole; a summary that took
3414
+ // several calls has been charged for the ones already finished.
3405
3415
  case "compact":
3406
3416
  await runBusy(COMPACTING, async (signal) => {
3407
3417
  const res = await compact(
3408
3418
  client,
3409
3419
  model,
3410
3420
  [{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
3421
+ contextWindow(model),
3411
3422
  { signal, effort: savedEffort(config, model) }
3412
3423
  );
3413
3424
  if (!res) return void push({ kind: "notice", text: " nothing to compact yet" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.48",
3
+ "version": "0.0.1-beta.49",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",