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.
- package/dist/agent/agent.js +230 -28
- package/dist/agent/llm.js +23 -0
- package/dist/model-meta.js +0 -6
- package/package.json +1 -1
- package/public/assets/{index-CzRDDcrd.js → index-C5nwg8RO.js} +4 -4
- package/public/assets/{index-CzRDDcrd.js.map → index-C5nwg8RO.js.map} +1 -1
- package/public/assets/{shiki-D5AInxu8.js → shiki-BSQfUh7j.js} +2 -2
- package/public/assets/{shiki-D5AInxu8.js.map → shiki-BSQfUh7j.js.map} +1 -1
- package/public/index.html +1 -1
package/dist/agent/agent.js
CHANGED
|
@@ -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
|
|
1912
|
-
//
|
|
1913
|
-
//
|
|
1914
|
-
//
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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
|
-
/**
|
|
3026
|
-
|
|
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
|
-
|
|
3044
|
-
|
|
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
|
-
|
|
3048
|
-
|
|
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
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
{
|
|
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)
|
package/dist/model-meta.js
CHANGED
|
@@ -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
|
-
}
|