teapot-coding-agent 0.29.8 → 0.29.10

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.
@@ -74,6 +74,43 @@ export const PROGRESS_REQUEST =
74
74
  * meta tools instead. The cache rationale itself stays HERE in a comment —
75
75
  * the model does not need our cost-engineering notes every turn.
76
76
  */
77
+ /**
78
+ * Recent context kept VERBATIM through a compaction, in tokens.
79
+ *
80
+ * Pi defaults `keepRecentTokens` to 20000 and opencode to `keep: 15000` for the
81
+ * same reason: the newest exchanges are what the model needs to continue
82
+ * accurately, and a verbatim copy beats a summary of them.
83
+ */
84
+ const KEEP_RECENT_TOKENS = 15_000;
85
+ /**
86
+ * The summarize call is skipped when the summarizable prefix is a small share of
87
+ * the budget, because the call cannot reuse any prefix cache (its system prompt is
88
+ * its instructions) and so re-bills the whole history uncached. Below this share,
89
+ * dropping the prefix outright reclaims the budget for free and the model can
90
+ * recover the details by re-reading the goal or the files.
91
+ *
92
+ * A SHARE rather than an absolute token count: what counts as "a lot to lose"
93
+ * scales with the window a session is configured for. An absolute constant
94
+ * calibrated for the 96k default would disable summarization entirely for a
95
+ * session with a small budget (measured: a 200-token budget with a 654-token
96
+ * history is over every bar, yet its prefix is a few hundred tokens).
97
+ *
98
+ * Manual compaction and overflow recovery bypass this — those are not cost
99
+ * decisions.
100
+ */
101
+ const SUMMARIZE_MIN_SHARE = 0.02;
102
+ /** the absolute floor for the bar above (see the note where it is applied) */
103
+ const SUMMARIZE_MIN_TOKENS = 2_000;
104
+ /**
105
+ * How much of the budget to KEEP on each successive overflow recovery.
106
+ *
107
+ * An overflow means the estimate was too low, so a single fixed cut can still
108
+ * leave the request too large — and the old code turned that second failure into a
109
+ * hard error. opencode uses the same shape (`SHRINK_STEPS [0.7, 0.5, 0.35]`): each
110
+ * retry keeps less, so the request shrinks even when the first attempt did not
111
+ * shrink it enough. The last entry repeats for any further attempt.
112
+ */
113
+ const OVERFLOW_SHRINK_STEPS = [0.5, 0.35, 0.2];
77
114
  const SYSTEM_TEMPLATE = `You are a coding agent working autonomously inside a workspace.
78
115
 
79
116
  Most session state is not injected into prompts — fetch it with tools instead:
@@ -334,6 +371,17 @@ export class Agent {
334
371
  skillsCache = [];
335
372
  /** fingerprint of the roots that produced skillsCache ("" = not scanned yet) */
336
373
  skillsStamp = "";
374
+ /**
375
+ * The notes the LAST compaction produced, if any.
376
+ *
377
+ * Compaction UPDATES these rather than re-deriving notes from scratch: a fresh
378
+ * summary each pass loses whatever the previous one distilled (every
379
+ * implementation surveyed — opencode, Kilo, Pi — updates for this reason).
380
+ * Recovered from the log so the continuity survives a restart.
381
+ */
382
+ lastCompactSummary;
383
+ /** overflow recoveries used in the current turn; reset when a turn succeeds */
384
+ overflowRecoveries = 0;
337
385
  /**
338
386
  * Rescan skill roots — but only when something actually changed (#14).
339
387
  * This used to run a readdir per root plus a read of every SKILL.md at the
@@ -538,6 +586,16 @@ export class Agent {
538
586
  * that wait would hang. isLive() answers "stop vs start" (#59/#133); this
539
587
  * answers "is work in flight".
540
588
  */
589
+ /**
590
+ * The text of every prompt still waiting for a turn boundary.
591
+ *
592
+ * Read-only view for callers that need to know WHAT is queued, not just how
593
+ * many — a delivery assertion has to see the text, or "the message arrived"
594
+ * degrades into "something is queued".
595
+ */
596
+ pendingPromptTexts() {
597
+ return this.pendingPrompts.map((p) => p.text);
598
+ }
541
599
  isBusy() {
542
600
  if (this.stopRequested)
543
601
  return false;
@@ -846,6 +904,13 @@ export class Agent {
846
904
  if (d.event === "context-compacted")
847
905
  this.stats.compactions++;
848
906
  }
907
+ else if (e.type === "compaction") {
908
+ // keep the NEWEST notes: they are what the next compaction updates, and
909
+ // without this a restart would silently fall back to re-summarizing
910
+ const d = e.data;
911
+ if (typeof d.summary === "string" && d.summary)
912
+ this.lastCompactSummary = d.summary;
913
+ }
849
914
  }
850
915
  // newest usage event seeds the context gauge right after reload —
851
916
  // inputTokens from the LAST turn is the live context size, and cached
@@ -1908,20 +1973,27 @@ export class Agent {
1908
1973
  detail: "stopped before any output arrived",
1909
1974
  });
1910
1975
  }
1911
- // Provider context overflow → compact ONCE and retry the turn
1912
- // (OpenCode-style overflow recovery). even with autoCompact off, an
1913
- // actual overflow is proof the estimate missed; a second overflow is
1914
- // returned as a real error instead of looping.
1976
+ // Provider context overflow → compact and retry (OpenCode-style recovery).
1977
+ // Even with autoCompact off, an actual overflow is proof the estimate
1978
+ // missed. Staged rather than single-shot (opencode: SHRINK_STEPS
1979
+ // [0.7, 0.5, 0.35]): one fixed cut can still be too shallow, and the old
1980
+ // code returned the second overflow as a hard error even though cutting
1981
+ // deeper would have worked.
1915
1982
  if (isContextOverflow(err) && !this.stopRequested) {
1983
+ const attempt = (this.overflowRecoveries += 1);
1916
1984
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
1917
1985
  event: "overflow-recovery",
1986
+ attempt,
1918
1987
  detail: String(err.message).slice(0, 200),
1919
1988
  });
1920
1989
  const before = this.stats.compactions;
1921
- await this.maybeCompact(true);
1990
+ // each retry cuts deeper: 1x, then the staged fractions of the budget
1991
+ const shrink = OVERFLOW_SHRINK_STEPS[Math.min(attempt - 1, OVERFLOW_SHRINK_STEPS.length - 1)];
1992
+ await this.maybeCompact(true, shrink);
1922
1993
  if (this.stats.compactions > before) {
1923
1994
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
1924
1995
  event: "overflow-recovered",
1996
+ attempt,
1925
1997
  });
1926
1998
  guard--; // the retry does not count against this round's cap
1927
1999
  continue;
@@ -1929,6 +2001,9 @@ export class Agent {
1929
2001
  }
1930
2002
  throw err;
1931
2003
  }
2004
+ // a request the provider accepted: the current depth is enough, so the next
2005
+ // overflow starts its staircase over rather than cutting from the deepest step
2006
+ this.overflowRecoveries = 0;
1932
2007
  if (res.usage) {
1933
2008
  const inTok = res.usage.inputTokens ?? 0;
1934
2009
  const cached = res.usage.cachedInputTokens ?? 0;
@@ -2898,7 +2973,7 @@ export class Agent {
2898
2973
  * token/char — the old flat /4 underestimated Japanese sessions ~4x.
2899
2974
  * Adds a small per-message overhead for role/framing tokens.
2900
2975
  */
2901
- estimateTokens() {
2976
+ estimateTokens(msgs = this.messages) {
2902
2977
  let ascii = 0;
2903
2978
  let wide = 0;
2904
2979
  const count = (s) => {
@@ -2909,14 +2984,14 @@ export class Agent {
2909
2984
  ascii++;
2910
2985
  }
2911
2986
  };
2912
- for (const m of this.messages) {
2987
+ for (const m of msgs) {
2913
2988
  count(m.content ?? "");
2914
2989
  for (const t of m.tool_calls ?? []) {
2915
2990
  count(t.function.name);
2916
2991
  count(t.function.arguments);
2917
2992
  }
2918
2993
  }
2919
- return Math.ceil(ascii / 4 + wide * 1.2 + this.messages.length * 8);
2994
+ return Math.ceil(ascii / 4 + wide * 1.2 + msgs.length * 8);
2920
2995
  }
2921
2996
  /**
2922
2997
  * Latest index whose message may START the kept tail of a compacted
@@ -3022,8 +3097,13 @@ export class Agent {
3022
3097
  });
3023
3098
  return count;
3024
3099
  }
3025
- /** Compact history when the real prompt size exceeds the budget. */
3026
- async maybeCompact(force = false) {
3100
+ /**
3101
+ * Compact history when the real prompt size exceeds the budget.
3102
+ *
3103
+ * @param keepFraction share of the budget to keep verbatim; the overflow path
3104
+ * lowers it on each retry so a too-shallow cut is not fatal
3105
+ */
3106
+ async maybeCompact(force = false, keepFraction) {
3027
3107
  // prefer the provider's own count from the last response — it is exactly
3028
3108
  // what would overflow the window; the char heuristic is only a fallback
3029
3109
  // for providers that omit usage
@@ -3040,12 +3120,27 @@ export class Agent {
3040
3120
  return;
3041
3121
  this.lastUsage = undefined; // stale after compaction — re-armed next turn
3042
3122
  // keep roughly the most recent quarter of the budget as live context
3043
- const keepCharBudget = (this.opts.contextTokenBudget / 4) * 4;
3044
- let keepChars = 0;
3123
+ // Keep the most recent stretch VERBATIM, as every implementation surveyed
3124
+ // does (Pi: keepRecentTokens 20000, opencode: keep 15000). Recency is what the
3125
+ // model needs to continue accurately, and verbatim beats a summary of it.
3126
+ //
3127
+ // Size: a quarter of the budget, but never less than KEEP_RECENT_TOKENS — a
3128
+ // small budget would otherwise keep so little that the model loses the turn it
3129
+ // is in the middle of. Measured in TOKENS rather than characters, so the choice
3130
+ // matches the published defaults instead of depending on the char heuristic.
3131
+ const keepTokens = keepFraction !== undefined
3132
+ // an explicit retry (overflow recovery): shrink toward a share of the budget
3133
+ ? Math.max(1, Math.floor(this.opts.contextTokenBudget * keepFraction))
3134
+ // otherwise keep the newest stretch verbatim, but never more than a quarter
3135
+ // of the budget — a fixed floor GREATER than the budget would keep everything
3136
+ // and compaction would never fire at all (measured: a 500-token budget kept
3137
+ // a 7.5k-token history intact)
3138
+ : Math.min(KEEP_RECENT_TOKENS, Math.max(1, Math.floor(this.opts.contextTokenBudget / 4)));
3045
3139
  let cut = -1;
3046
3140
  for (let i = this.messages.length - 1; i >= 1; i--) {
3047
- keepChars += (this.messages[i].content?.length ?? 0) + JSON.stringify(this.messages[i].tool_calls ?? "").length;
3048
- if (keepChars > keepCharBudget)
3141
+ // token-aware boundary, using the SAME estimate the budget uses — a raw char
3142
+ // ratio would disagree with it for wide (CJK) text and per-message overhead
3143
+ if (this.estimateTokens(this.messages.slice(i)) > keepTokens)
3049
3144
  break;
3050
3145
  cut = i;
3051
3146
  }
@@ -3054,10 +3149,62 @@ export class Agent {
3054
3149
  return; // nothing safely compactable (phase never armed yet)
3055
3150
  const old = this.messages.slice(0, cut);
3056
3151
  const oldCount = old.length;
3152
+ // The prefix's share of the size the budget check ACTED on (which floors our
3153
+ // estimate against the provider's reported prompt_tokens). Scaling by character
3154
+ // share rather than message count: messages vary hugely in size, and the point
3155
+ // is to avoid treating an unmeasurably large context as small.
3156
+ const totalChars = this.messages.reduce((n, m) => n + (m.content?.length ?? 0), 0);
3157
+ const oldChars = old.reduce((n, m) => n + (m.content?.length ?? 0), 0);
3158
+ const oldShareOfBefore = totalChars > 0 ? Math.round((before * oldChars) / totalChars) : 0;
3057
3159
  let summary = "";
3058
3160
  let mode = "summarize";
3059
3161
  let summarizedCount = oldCount;
3060
3162
  let droppedCount = 0;
3163
+ // COST GATE (measured). The summarize call shares NO cacheable prefix with the
3164
+ // main loop: the summarizer's system prompt IS its instructions, so message[0]
3165
+ // differs and the entire history is re-prefilled and re-billed at full size.
3166
+ // Sharing the prefix would mean gutting the summarizer's role, so the lever is
3167
+ // not to PAY for a shallow saving.
3168
+ //
3169
+ // The criterion is the ABSOLUTE amount of context the cut would discard, not
3170
+ // how far over the budget we are. A ratio test gets it backwards: a small
3171
+ // window overflowing slightly is exactly the case where a full-history call
3172
+ // buys almost nothing (there is little knowledge to lose), while a large
3173
+ // window overflowing slightly still has a lot to lose. `oldTokens` is the
3174
+ // summarizable prefix — the thing the call is actually paid for.
3175
+ //
3176
+ // Below the threshold the DROP alone reclaims the budget, for free.
3177
+ //
3178
+ // MANUAL is exempt: an explicit /compact is a user command, not a cost
3179
+ // decision, and opencode draws the line in the same place ("only the user
3180
+ // compacts when automatic compaction is off"). An overflow recovery is exempt
3181
+ // too — the provider just told us the prompt was too big, so the question is
3182
+ // not whether the saving is worth a call.
3183
+ // Use BOTH sizes. `oldTokens` is our estimate of the prefix being summarized;
3184
+ // `before` is the size the budget check acted on, which already floors that
3185
+ // estimate against the provider's REPORTED prompt_tokens. Gating on the local
3186
+ // estimate alone would silently truncate a context the provider says is large
3187
+ // but we cannot measure (caught by a test: provider reports 5000 tokens for a
3188
+ // 300-token local estimate). The provider tokenizes for real; our count is a
3189
+ // guess, so the larger of the two is the honest figure.
3190
+ const oldTokens = Math.max(this.estimateTokens(old), oldShareOfBefore);
3191
+ // The bar is a SHARE of the budget with an absolute floor, so it scales with a
3192
+ // realistic window while never sitting above a small session's entire history
3193
+ // (measured: a budget of 200 made a pure-share bar 10 tokens, while the whole
3194
+ // summarizable prefix in that fixture was an 8-token prompt — summarization
3195
+ // would have been silently disabled).
3196
+ //
3197
+ // Below the bar the prefix is a turn or two, and dropping it costs the model
3198
+ // nothing it cannot recover by re-reading the goal or the files; a full-history
3199
+ // LLM call to preserve that is not worth its price.
3200
+ const summarizeBar = Math.max(SUMMARIZE_MIN_TOKENS, this.opts.contextTokenBudget * SUMMARIZE_MIN_SHARE);
3201
+ const worthSummarizing = force || oldTokens >= summarizeBar;
3202
+ // UPDATE mode: carry the previous notes forward instead of re-deriving them.
3203
+ // Every implementation surveyed (opencode, Kilo, Pi) updates rather than
3204
+ // re-summarizes, because a fresh summary each time loses whatever the previous
3205
+ // pass had already distilled. Recovered from the LAST compaction event, so it
3206
+ // survives a restart.
3207
+ const previousSummary = this.lastCompactSummary;
3061
3208
  // progress is visible: announce the phase up front, then stream the
3062
3209
  // summarizer's output as it generates (a long summarize used to look like
3063
3210
  // the agent silently hung — hundreds of thousands of tokens take a while)
@@ -3091,18 +3238,32 @@ export class Agent {
3091
3238
  tokensBefore: before,
3092
3239
  messages: oldCount,
3093
3240
  reason: force ? "manual" : "auto",
3241
+ worthSummarizing, // makes the cost decision auditable
3242
+ updating: Boolean(previousSummary),
3094
3243
  });
3095
3244
  try {
3096
- summary = await this.summarize(old, (text) => {
3097
- bus.emit("update", {
3098
- kind: "llm-delta",
3099
- agentId: this.opts.id,
3100
- // #146: the bubble is session-scoped, so the stream must say which session
3101
- sessionId: this.currentSession,
3102
- text: `[compact] ${text}`,
3103
- reasoning: "",
3245
+ if (worthSummarizing) {
3246
+ summary = await this.summarize(old, (text) => {
3247
+ bus.emit("update", {
3248
+ kind: "llm-delta",
3249
+ agentId: this.opts.id,
3250
+ // #146: the bubble is session-scoped, so the stream must say which session
3251
+ sessionId: this.currentSession,
3252
+ text: `[compact] ${text}`,
3253
+ reasoning: "",
3254
+ });
3255
+ }, previousSummary);
3256
+ }
3257
+ else {
3258
+ // shallow saving: truncation reclaims the budget for free, so skip the
3259
+ // one full-history LLM call the summary would cost
3260
+ await this.log.append("system_note", this.currentSession, this.currentBranch, {
3261
+ event: "context-compaction-skipped-summarize",
3262
+ reason: "shallow: truncation reclaims the budget without a full-history LLM call",
3263
+ tokensBefore: before,
3264
+ budget: this.opts.contextTokenBudget,
3104
3265
  });
3105
- });
3266
+ }
3106
3267
  this.compactPhase = "harvesting";
3107
3268
  bus.emit("update", { kind: "compaction-progress", agentId: this.opts.id, phase: "harvesting" });
3108
3269
  if (summary)
@@ -3135,7 +3296,23 @@ export class Agent {
3135
3296
  return; // the finally clears the phase and tells the UI it ended
3136
3297
  }
3137
3298
  droppedCount = halfCut;
3138
- this.messages = this.messages.slice(halfCut);
3299
+ // Tell the model its history was cut. Dropping a prefix SILENTLY is the
3300
+ // one failure it cannot detect or work around: it would see a conversation
3301
+ // that simply starts mid-way and could not distinguish "I forgot this" from
3302
+ // "this never happened", then confidently act on a gap. Truncation is now
3303
+ // the COMMON path (the cost gate keeps the summarize call for deep cuts
3304
+ // only), which makes this the difference between a known gap and a wrong
3305
+ // belief. The dropped text stays in the log — nothing durable is lost.
3306
+ this.messages = [
3307
+ {
3308
+ role: "user",
3309
+ content: `[harness] ${droppedCount} earlier messages were DROPPED, not summarized (no room to ` +
3310
+ "summarize, or the saving would not have justified the call). Your earlier work is " +
3311
+ "in the session log but is NOT in your context any more: re-read files or the goal " +
3312
+ "before assuming what happened, and do not rely on memory of the dropped part.",
3313
+ },
3314
+ ...this.messages.slice(halfCut),
3315
+ ];
3139
3316
  }
3140
3317
  else {
3141
3318
  this.messages = [
@@ -3216,6 +3393,9 @@ export class Agent {
3216
3393
  mode,
3217
3394
  ...(summary ? { summary: String(summary).slice(0, 20_000) } : {}),
3218
3395
  });
3396
+ // remember the notes for the NEXT compaction to update (and for a restore)
3397
+ if (summary)
3398
+ this.lastCompactSummary = summary;
3219
3399
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
3220
3400
  event: "context-compacted",
3221
3401
  tokensBefore: before,
@@ -3235,7 +3415,7 @@ export class Agent {
3235
3415
  }
3236
3416
  }
3237
3417
  /** Ask the model for dense continuation notes over the compacted range. */
3238
- async summarize(old, onDelta) {
3418
+ async summarize(old, onDelta, previousSummary) {
3239
3419
  const full = old
3240
3420
  .map((m) => {
3241
3421
  const who = m.role === "tool" ? "tool" : m.role;
@@ -3276,10 +3456,25 @@ export class Agent {
3276
3456
  // where "pending tasks" vs "user messages" live, so nothing silently
3277
3457
  // merges away. The <analysis> scratchpad gives the model a thinking space;
3278
3458
  // it is stripped from the stored summary (formatCompactSummary).
3279
- const res = await fn(this.opts.llm, [
3459
+ const res = await fn({ ...this.opts.llm, oneOff: true }, // one-off prompt: ask the provider not to retain it
3460
+ [
3280
3461
  {
3281
3462
  role: "system",
3282
- content: "You compress a coding agent's conversation into dense notes for it to continue working.\n\n" +
3463
+ content:
3464
+ // Both variants OPEN with the same sentence. Tests (and any future caller)
3465
+ // identify the summarizer by this text, and a shared prefix keeps that
3466
+ // detection working across the initial/update split — the first version of
3467
+ // update mode changed it to "You UPDATE…", which silently made every
3468
+ // update pass look like an ordinary turn.
3469
+ "You compress a coding agent's conversation into dense notes for it to continue working.\n\n" +
3470
+ (previousSummary
3471
+ ? "This is an UPDATE: the current notes are under `## Current notes`. They are your\n" +
3472
+ "output from an earlier compaction and are the ONLY record of everything before the\n" +
3473
+ "messages you are about to read — so carry them forward. PRESERVE what is still\n" +
3474
+ "true, ADD what the new messages establish, and REMOVE only what they resolve or\n" +
3475
+ "supersede. Never drop a constraint or a user request just because the new\n" +
3476
+ "messages do not repeat it: an unanswered request stays, an answered one goes.\n\n"
3477
+ : "") +
3283
3478
  "First think inside an <analysis> block: what mattered, what changed, what is unresolved.\n" +
3284
3479
  "Then output ONLY the following sections, each starting with its exact heading:\n\n" +
3285
3480
  "## Primary request and intent\n" +
@@ -3297,7 +3492,14 @@ export class Agent {
3297
3492
  'Durable lessons lists reusable insights worth keeping forever (gotchas, preferences, what worked); omit the section if empty. ' +
3298
3493
  "The <analysis> block is discarded before your notes are stored, so think freely there.",
3299
3494
  },
3300
- { role: "user", content: `Conversation:\n\n${transcript}\n\nWrite the continuation notes now.` },
3495
+ {
3496
+ role: "user",
3497
+ content: (previousSummary ? `## Current notes\n\n${previousSummary}\n\n` : "") +
3498
+ `## Conversation\n\n${transcript}\n\n` +
3499
+ (previousSummary
3500
+ ? "Update the current notes to cover this conversation as well. Return the FULL updated notes."
3501
+ : "Write the continuation notes now."),
3502
+ },
3301
3503
  ], [], undefined, // never abortable by stop(): losing the summary would lose history
3302
3504
  onDelta
3303
3505
  ? (s) => onDelta(s.text)
package/dist/agent/llm.js CHANGED
@@ -37,6 +37,27 @@ function isOpenRouterEndpoint(baseUrl) {
37
37
  return false;
38
38
  }
39
39
  }
40
+ /**
41
+ * Ask the endpoint NOT to retain a one-off request (Pi does the same with
42
+ * `cacheRetention: "none"`; its docs say "summarization requests disable
43
+ * prompt-cache writes because these one-off prompts are unlikely to be reused").
44
+ *
45
+ * Sent ONLY to OpenRouter, which is the one endpoint in use here that both
46
+ * accepts the field and documents it. Every other OpenAI-compatible endpoint gets
47
+ * nothing: an unrecognised body field is a 400 on strict providers, and "send
48
+ * nothing" is exactly today's behaviour, so this can only ever add a saving.
49
+ *
50
+ * `24h` is the value that means "do not retain beyond this request's lifetime" for
51
+ * an organisation with zero-data-retention off; with ZDR on the default is already
52
+ * `in_memory` and this is a no-op.
53
+ */
54
+ export function cacheControlForRequest(cfg) {
55
+ if (!cfg.oneOff)
56
+ return {};
57
+ if (!isOpenRouterEndpoint(cfg.baseUrl))
58
+ return {};
59
+ return { prompt_cache_retention: "24h" };
60
+ }
40
61
  const REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
41
62
  /**
42
63
  * OpenRouter app attribution (https://openrouter.ai/docs/app-attribution):
@@ -194,6 +215,7 @@ export async function chat(cfg, messages, tools, signal, onDelta) {
194
215
  messages: sanitize(messages),
195
216
  ...(tools.length ? { tools } : {}),
196
217
  ...effortForRequest(cfg),
218
+ ...cacheControlForRequest(cfg),
197
219
  }, { signal });
198
220
  // #50: a gateway may answer with `null` (or any non-object) on some proxy
199
221
  // error paths, so the guard is on `rm`, not on `res` being truthy —
@@ -289,6 +311,7 @@ export async function chatStream(cfg, messages, tools, signal, onDelta) {
289
311
  messages: sanitize(messages),
290
312
  ...(tools.length ? { tools } : {}),
291
313
  ...effortForRequest(cfg),
314
+ ...cacheControlForRequest(cfg),
292
315
  stream: true,
293
316
  // ask providers to include the final usage chunk in streaming mode
294
317
  // (OpenAI-compatible; harmless where unsupported)
@@ -3,7 +3,6 @@
3
3
  * Used to auto-populate contextWindowTokens so compaction budgets derive
4
4
  * from the real window instead of the safe default.
5
5
  */
6
- import fs from "node:fs";
7
6
  import { providerHeaders } from "./agent/llm.js";
8
7
  const cache = new Map();
9
8
  const inFlight = new Map();
@@ -155,8 +154,3 @@ export function resolveReasoningEffort(opts) {
155
154
  return undefined;
156
155
  return opts.effort;
157
156
  }
158
- /** test hook */
159
- export function clearModelListCache() {
160
- cache.clear();
161
- void fs;
162
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.29.8",
3
+ "version": "0.29.10",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",