one-code-extension 0.1.4 → 0.3.1
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/LICENSE +20 -0
- package/README.md +222 -177
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +128 -0
- package/docs/guide/configuration.md +173 -0
- package/docs/guide/differences-from-claude-code.md +152 -0
- package/docs/guide/doctor.md +88 -0
- package/docs/guide/hooks.md +111 -0
- package/docs/guide/installation.md +126 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
- package/docs/guide/providers-and-models.md +196 -0
- package/docs/guide/reference.md +226 -0
- package/docs/guide/sessions-and-context.md +192 -0
- package/docs/guide/skills-plugins-and-mcp.md +178 -0
- package/docs/guide/subagents-and-workflows.md +195 -0
- package/docs/guide/tasks-and-background-work.md +113 -0
- package/docs/guide/terminal-interface.md +157 -0
- package/docs/guide/tools.md +177 -0
- package/docs/guide/troubleshooting.md +177 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -28,16 +28,20 @@
|
|
|
28
28
|
* prefix thinking-cleared while this call sends full thinking blocks, and the
|
|
29
29
|
* mismatch invalidates the message-block cache (system+tools still read).
|
|
30
30
|
*
|
|
31
|
+
* Not every compaction can replay — `fit.ts` explains when and why the
|
|
32
|
+
* standalone shape (the doomed span alone, trimmed to the window) is used.
|
|
33
|
+
*
|
|
31
34
|
* Any failure returns nothing, so pi's own compaction serves — a different
|
|
32
35
|
* summary style, never a broken compaction. CC_COMPACTION=0 opts out.
|
|
33
36
|
*/
|
|
34
37
|
|
|
35
38
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
36
|
-
import type { Api, Model, Tool } from "@earendil-works/pi-ai";
|
|
39
|
+
import type { Api, Context, Message, Model, Tool } from "@earendil-works/pi-ai";
|
|
37
40
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
38
41
|
import {
|
|
39
42
|
convertToLlm,
|
|
40
43
|
type ExtensionAPI,
|
|
44
|
+
type ExtensionContext,
|
|
41
45
|
type SessionBeforeCompactEvent,
|
|
42
46
|
} from "@earendil-works/pi-coding-agent";
|
|
43
47
|
import {
|
|
@@ -46,14 +50,26 @@ import {
|
|
|
46
50
|
clearThinkingApplies,
|
|
47
51
|
clearThinkingEnabled,
|
|
48
52
|
isAnthropicOAuth,
|
|
49
|
-
looksLikeAnthropicRequest,
|
|
50
53
|
withClearThinking,
|
|
51
54
|
} from "../context-management/index.ts";
|
|
55
|
+
import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
|
|
56
|
+
import { forcedReasoningLevel } from "../lib/model-policy.ts";
|
|
57
|
+
import { fitToBudget, replayFits, withoutUsage } from "./fit.ts";
|
|
52
58
|
import { buildCompactionInstruction, COMPACTION_MAX_TOKENS, continuationSummary, extractSummary } from "./prompt.ts";
|
|
53
59
|
|
|
54
60
|
/** Compaction reads a whole context window; give it more room than the classifier's 30s. */
|
|
55
61
|
const COMPACTION_TIMEOUT_MS = 120_000;
|
|
56
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The system prompt of a standalone (non-replay) request. The session's own is
|
|
65
|
+
* left out along with the tools: neither helps a request that cannot hit the
|
|
66
|
+
* cache, and together they cost ~20k tokens the doomed span needs on an overflow.
|
|
67
|
+
*/
|
|
68
|
+
export const STANDALONE_SYSTEM_PROMPT =
|
|
69
|
+
"You are the coding assistant from the conversation below, asked to summarize it so work can continue in a fresh context. Respond with text only.";
|
|
70
|
+
|
|
71
|
+
type Preparation = Pick<SessionBeforeCompactEvent["preparation"], "messagesToSummarize" | "turnPrefixMessages" | "isSplitTurn" | "previousSummary">;
|
|
72
|
+
|
|
57
73
|
export default function compactionExtension(pi: ExtensionAPI) {
|
|
58
74
|
/**
|
|
59
75
|
* The exact message array the session last handed the provider, captured
|
|
@@ -68,8 +84,8 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
68
84
|
* `convertToLlm` on the captured array, so the message prefix we send is
|
|
69
85
|
* byte-identical to the turn's, and appending only the instruction keeps that
|
|
70
86
|
* whole prefix reusable. Reconstructing from session entries instead (the
|
|
71
|
-
*
|
|
72
|
-
* become entries — so it cannot reproduce the cached prefix at all.
|
|
87
|
+
* standalone path) diverges at message one, because those injected reminders
|
|
88
|
+
* never become entries — so it cannot reproduce the cached prefix at all.
|
|
73
89
|
*
|
|
74
90
|
* We hold the reference, not a copy: `emitContext` hands each handler a fresh
|
|
75
91
|
* structuredClone the session never mutates again, and compaction is the
|
|
@@ -80,39 +96,53 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
80
96
|
pi.on("context", (event) => {
|
|
81
97
|
capturedMessages = event.messages;
|
|
82
98
|
});
|
|
99
|
+
// After a compaction the capture describes the pre-compaction request. A
|
|
100
|
+
// second /compact before any turn would otherwise re-summarize history the
|
|
101
|
+
// first one already folded away (and mis-scope the kept tail); the standalone
|
|
102
|
+
// reconstruction from entries serves until the next request recaptures.
|
|
103
|
+
pi.on("session_compact", () => {
|
|
104
|
+
capturedMessages = undefined;
|
|
105
|
+
});
|
|
106
|
+
// A /tree branch switch stays in the same process and fires no `context`
|
|
107
|
+
// event (navigateTree rebuilds the messages itself), so the capture would
|
|
108
|
+
// still describe the abandoned branch — and a /compact before the next turn
|
|
109
|
+
// summarized work the kept branch never did (review H1). Same remedy.
|
|
110
|
+
pi.on("session_tree", () => {
|
|
111
|
+
capturedMessages = undefined;
|
|
112
|
+
});
|
|
83
113
|
|
|
84
114
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
85
115
|
if (process.env.CC_COMPACTION === "0") return undefined;
|
|
86
116
|
|
|
87
117
|
const model = ctx.model;
|
|
88
118
|
if (!model) return undefined;
|
|
119
|
+
// Snapshot before the first await so the capture and pi's preparation
|
|
120
|
+
// describe the same request even if a context event lands meanwhile.
|
|
121
|
+
const captured = capturedMessages;
|
|
89
122
|
|
|
90
123
|
try {
|
|
91
124
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
92
125
|
if (!auth.ok) return undefined;
|
|
93
126
|
const baseUrl = (auth as { baseUrl?: string }).baseUrl;
|
|
94
127
|
|
|
95
|
-
|
|
96
|
-
// carries the leading compactionSummary message on a re-compaction.
|
|
97
|
-
// Fall back to reconstructing from session entries only when nothing
|
|
98
|
-
// has been captured yet (e.g. /compact as the first action in a freshly
|
|
99
|
-
// resumed session, before any turn ran); that path cannot hit the cache.
|
|
100
|
-
const messages = capturedMessages ?? reconstructFromEntries(event.preparation);
|
|
101
|
-
|
|
102
|
-
const instruction = buildCompactionInstruction({
|
|
103
|
-
reason: event.reason,
|
|
104
|
-
customInstructions: event.customInstructions,
|
|
105
|
-
});
|
|
128
|
+
const maxTokens = Math.min(COMPACTION_MAX_TOKENS, model.maxTokens || COMPACTION_MAX_TOKENS);
|
|
106
129
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
// Prefer the captured request prefix — it is cache-aligned and already
|
|
131
|
+
// carries the leading compactionSummary message on a re-compaction — as
|
|
132
|
+
// long as pi's max_tokens clamp leaves it room for the reply (fit.ts).
|
|
133
|
+
// Otherwise (no capture yet, an overflow, a nearly full window) the
|
|
134
|
+
// standalone shape summarizes the doomed span pi isolated. An overflow
|
|
135
|
+
// never replays, so its replay request is not even built.
|
|
136
|
+
const replay = captured && event.reason !== "overflow" ? replayRequest(pi, ctx, captured, event) : undefined;
|
|
137
|
+
const request =
|
|
138
|
+
replay && replayFits(event.reason, model, replay, maxTokens)
|
|
139
|
+
? replay
|
|
140
|
+
: standaloneRequest(
|
|
141
|
+
model,
|
|
142
|
+
event.preparation,
|
|
143
|
+
buildCompactionInstruction({ reason: event.reason, customInstructions: event.customInstructions }),
|
|
144
|
+
maxTokens,
|
|
145
|
+
);
|
|
116
146
|
|
|
117
147
|
// When context-management (clear_thinking) is active for this session,
|
|
118
148
|
// the agent loop's cached message prefix has old thinking blocks cleared
|
|
@@ -134,48 +164,42 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
134
164
|
: auth.headers;
|
|
135
165
|
|
|
136
166
|
const timeout = AbortSignal.timeout(COMPACTION_TIMEOUT_MS);
|
|
137
|
-
const result = await completeSimple(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
clearThinkingApplies(payload as Record<string, unknown>, cmModel.compat?.forceAdaptiveThinking === true)
|
|
174
|
-
? withClearThinking(payload as Record<string, unknown>)
|
|
175
|
-
: payload
|
|
176
|
-
: undefined,
|
|
177
|
-
},
|
|
178
|
-
);
|
|
167
|
+
const result = await completeSimple(baseUrl ? ({ ...model, baseUrl } as Model<Api>) : model, request, {
|
|
168
|
+
apiKey: auth.apiKey,
|
|
169
|
+
headers,
|
|
170
|
+
env: auth.env,
|
|
171
|
+
signal: AbortSignal.any([event.signal, timeout]),
|
|
172
|
+
maxTokens,
|
|
173
|
+
// The session id is the prompt-cache key / routing affinity on the
|
|
174
|
+
// providers that use one (openai-codex prompt_cache_key, session
|
|
175
|
+
// headers). Without it the replayed prefix cannot hit the session's
|
|
176
|
+
// cache no matter how well the bytes match.
|
|
177
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
178
|
+
// Reasoning config is part of the cache identity on the Responses
|
|
179
|
+
// API: with a byte-identical prefix, prompt_cache_key, tools, and
|
|
180
|
+
// system prompt, omitting `reasoning` on a reasoning-configured
|
|
181
|
+
// session still misses the cache entirely (measured: cacheRead 0).
|
|
182
|
+
// Mirror the session's own thinking level so the request matches
|
|
183
|
+
// the prefix the session cached. Unlike the auto-mode classifier —
|
|
184
|
+
// which may run a *different*, unvalidated model where reasoning
|
|
185
|
+
// fails closed — this is the session's own model, already proven to
|
|
186
|
+
// accept this level, and streamSimple clamps an unsupported level
|
|
187
|
+
// down rather than erroring, so mirroring never fails closed here.
|
|
188
|
+
// "off" carries no reasoning (matching what such a session sends) —
|
|
189
|
+
// unless the model cannot disable thinking, where an off-request
|
|
190
|
+
// would 400 (forcedReasoningLevel sends its lowest level instead).
|
|
191
|
+
reasoning: ctx.thinkingLevel === "off" ? forcedReasoningLevel(model) : ctx.thinkingLevel,
|
|
192
|
+
// The clear_thinking body edit, matching the session's requests (see
|
|
193
|
+
// the header note above). Only attached on Anthropic requests that
|
|
194
|
+
// carry thinking, exactly as the context-management extension gates it.
|
|
195
|
+
onPayload: clearThinking
|
|
196
|
+
? (payload: unknown) =>
|
|
197
|
+
looksLikeAnthropicRequest(payload) &&
|
|
198
|
+
clearThinkingApplies(payload as Record<string, unknown>, cmModel.compat?.forceAdaptiveThinking === true)
|
|
199
|
+
? withClearThinking(payload as Record<string, unknown>)
|
|
200
|
+
: payload
|
|
201
|
+
: undefined,
|
|
202
|
+
});
|
|
179
203
|
|
|
180
204
|
const text = result.content
|
|
181
205
|
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
@@ -201,10 +225,91 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
201
225
|
}
|
|
202
226
|
|
|
203
227
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
228
|
+
* The cache-aligned shape: the session's system prompt, the active tool
|
|
229
|
+
* definitions in their active order — kept purely so the cached prefix (tools
|
|
230
|
+
* come first) still matches; a mismatch costs cache hits, never correctness —
|
|
231
|
+
* the captured request verbatim, and the instruction as one more user message.
|
|
232
|
+
*
|
|
233
|
+
* pi keeps the tail after the cut point verbatim (keepRecentTokens, ~20k
|
|
234
|
+
* tokens); the captured request still carries that tail, so the instruction
|
|
235
|
+
* names it (`keptTailOf`). Cutting the tail off the request instead was
|
|
236
|
+
* rejected: Anthropic checks cache hits only ~20 blocks back from the
|
|
237
|
+
* breakpoint, so a request ending well before the last cached block misses
|
|
238
|
+
* the cache the whole replay exists to hit.
|
|
239
|
+
*/
|
|
240
|
+
function replayRequest(pi: ExtensionAPI, ctx: ExtensionContext, captured: AgentMessage[], event: SessionBeforeCompactEvent): Context {
|
|
241
|
+
const byName = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
|
|
242
|
+
const tools = pi
|
|
243
|
+
.getActiveTools()
|
|
244
|
+
.map((name) => byName.get(name))
|
|
245
|
+
.filter((tool) => tool !== undefined)
|
|
246
|
+
.map(({ name, description, parameters }) => ({ name, description, parameters }) as Tool);
|
|
247
|
+
const instruction = buildCompactionInstruction({
|
|
248
|
+
reason: event.reason,
|
|
249
|
+
customInstructions: event.customInstructions,
|
|
250
|
+
keptTail: keptTailOf(captured, event.preparation),
|
|
251
|
+
});
|
|
252
|
+
return { systemPrompt: ctx.getSystemPrompt(), messages: [...convertToLlm(captured), instructionMessage(instruction)], tools };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The standalone shape: the doomed span reconstructed from session entries,
|
|
257
|
+
* its assistant usage zeroed and its largest tool results cleared until pi's
|
|
258
|
+
* clamp leaves room for the summary (`fit.ts`), no tools, a one-line system
|
|
259
|
+
* prompt. Gets no kept-tail note: it holds only the doomed span. If it fails,
|
|
260
|
+
* pi's own compaction serves. Exported for the unit test.
|
|
261
|
+
*/
|
|
262
|
+
export function standaloneRequest(model: Model<Api>, preparation: Preparation, instruction: string, maxTokens: number): Context {
|
|
263
|
+
const messages = [...withoutUsage(convertToLlm(reconstructFromEntries(preparation))), instructionMessage(instruction)];
|
|
264
|
+
return fitToBudget(model, { systemPrompt: STANDALONE_SYSTEM_PROMPT, messages }, maxTokens).request;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const instructionMessage = (instruction: string): Message => ({ role: "user", content: instruction, timestamp: Date.now() });
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The verbatim-kept tail of the captured request: everything after the doomed
|
|
271
|
+
* span. pi builds the context as `[compactionSummary?] + one message per entry`
|
|
272
|
+
* (`sessionEntryToContextMessages`) and `prepareCompaction` derives
|
|
273
|
+
* `messagesToSummarize`/`turnPrefixMessages` with the same mapping, so the
|
|
274
|
+
* doomed span is a prefix of the captured array. Alignment is checked role by
|
|
275
|
+
* role; on any mismatch (an extension that inserted a message) no note is made
|
|
276
|
+
* rather than a wrong one. Exported for the unit test.
|
|
277
|
+
*/
|
|
278
|
+
export function keptTailOf(
|
|
279
|
+
captured: readonly { role: string; content?: unknown }[],
|
|
280
|
+
preparation: Preparation,
|
|
281
|
+
): { count: number; opening?: string } | undefined {
|
|
282
|
+
const doomed: { role: string }[] = [
|
|
283
|
+
...(preparation.previousSummary ? [{ role: "compactionSummary" }] : []),
|
|
284
|
+
...preparation.messagesToSummarize,
|
|
285
|
+
...(preparation.isSplitTurn ? preparation.turnPrefixMessages : []),
|
|
286
|
+
];
|
|
287
|
+
if (doomed.length === 0 || doomed.length > captured.length) return undefined;
|
|
288
|
+
for (let i = 0; i < doomed.length; i++) {
|
|
289
|
+
if (captured[i].role !== doomed[i].role) return undefined;
|
|
290
|
+
}
|
|
291
|
+
const count = captured.length - doomed.length;
|
|
292
|
+
if (count === 0) return { count };
|
|
293
|
+
return { count, opening: openingText(captured[doomed.length]) };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The first ~80 characters of a message's text, single-line, for the landmark quote. */
|
|
297
|
+
function openingText(message: { content?: unknown }): string | undefined {
|
|
298
|
+
const content = message.content;
|
|
299
|
+
const text =
|
|
300
|
+
typeof content === "string"
|
|
301
|
+
? content
|
|
302
|
+
: Array.isArray(content)
|
|
303
|
+
? (content.find((b) => b && typeof b === "object" && (b as { type?: string }).type === "text") as { text?: string } | undefined)?.text
|
|
304
|
+
: undefined;
|
|
305
|
+
if (!text) return undefined;
|
|
306
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
307
|
+
return line.length > 80 ? `${line.slice(0, 80)}…` : line;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The doomed span as messages, from session entries — the standalone path's
|
|
312
|
+
* input.
|
|
208
313
|
*
|
|
209
314
|
* A previous compaction's summary is excluded from messagesToSummarize
|
|
210
315
|
* (prepareCompaction starts after that entry), yet the live context carries it
|
|
@@ -213,7 +318,7 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
213
318
|
* turn's discarded prefix has no separate summary on this path, so it is
|
|
214
319
|
* summarized with the rest.
|
|
215
320
|
*/
|
|
216
|
-
function reconstructFromEntries(preparation:
|
|
321
|
+
function reconstructFromEntries(preparation: Preparation): AgentMessage[] {
|
|
217
322
|
const doomed = preparation.isSplitTurn
|
|
218
323
|
? [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages]
|
|
219
324
|
: preparation.messagesToSummarize;
|
|
@@ -132,6 +132,13 @@ REMINDER: Do NOT call any tools. Respond with plain text only — an <analysis>
|
|
|
132
132
|
export function buildCompactionInstruction(options: {
|
|
133
133
|
reason: "manual" | "threshold" | "overflow";
|
|
134
134
|
customInstructions?: string;
|
|
135
|
+
/**
|
|
136
|
+
* How many trailing messages of the conversation pi keeps verbatim after
|
|
137
|
+
* the cut (its `keepRecentTokens` tail). When set and > 0 the instruction
|
|
138
|
+
* says so, so the summary covers the part that is actually discarded
|
|
139
|
+
* instead of re-telling what the model will still see in full.
|
|
140
|
+
*/
|
|
141
|
+
keptTail?: { count: number; opening?: string };
|
|
135
142
|
}): string {
|
|
136
143
|
const notice =
|
|
137
144
|
options.reason === "manual"
|
|
@@ -141,10 +148,23 @@ export function buildCompactionInstruction(options: {
|
|
|
141
148
|
if (options.customInstructions?.trim()) {
|
|
142
149
|
parts.push(`<system-reminder>\n## Compact Instructions\n${options.customInstructions.trim()}\n</system-reminder>`);
|
|
143
150
|
}
|
|
144
|
-
|
|
151
|
+
const tail = options.keptTail && options.keptTail.count > 0 ? keptTailNote(options.keptTail) : "";
|
|
152
|
+
parts.push(`<system-reminder>\n${notice}${tail}\n${COMPACTION_INSTRUCTION}\n</system-reminder>`);
|
|
145
153
|
return parts.join("\n\n");
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The scope note for pi's kept tail: the summary replaces only what precedes
|
|
158
|
+
* it. Quoting how the first kept message opens gives the model a landmark it
|
|
159
|
+
* can find in the transcript, since a bare count is easy to miscount.
|
|
160
|
+
*/
|
|
161
|
+
export function keptTailNote(tail: { count: number; opening?: string }): string {
|
|
162
|
+
const landmark = tail.opening?.trim() ? `, beginning with the message that opens "${tail.opening.trim()}"` : "";
|
|
163
|
+
return ` Only the earlier part of the conversation is being summarized: the final ${tail.count} ${
|
|
164
|
+
tail.count === 1 ? "message" : "messages"
|
|
165
|
+
}${landmark}, stay in context verbatim after the summary. Do not summarize them; cover everything before them, and use them only to judge which earlier details still matter.`;
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
/**
|
|
149
169
|
* Claude Code's continuation preamble, baked into the *stored* summary so
|
|
150
170
|
* every future context — and the reattached compactionSummary message on a
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's `<total_tokens>N tokens left</total_tokens>` signal, pure half.
|
|
3
|
+
*
|
|
4
|
+
* What the number is (read off a live Claude Code session and the 2.1.261
|
|
5
|
+
* capture, findings §14): a PER-TURN token budget, not the context window. It
|
|
6
|
+
* reads 15,000,000 at every turn start — the last line of the system prompt
|
|
7
|
+
* before the gitStatus block, and the end of every user message — and counts
|
|
8
|
+
* down on the tool results within the turn by roughly what each round added
|
|
9
|
+
* (tool results, assistant output); the next user message resets it. Constant
|
|
10
|
+
* at the cached-prefix positions, variable only on the tail, so it never busts
|
|
11
|
+
* the prefix. One Code approximates the countdown as the growth of the context
|
|
12
|
+
* since the turn started (pi's context-usage estimate), the same quantity to
|
|
13
|
+
* within the estimator's error. `CC_TOTAL_TOKENS_BUDGET` overrides the figure.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Claude Code's per-turn budget as observed; the meaning of the figure is CC's max tokens per turn with compaction. */
|
|
17
|
+
export const DEFAULT_TURN_TOKEN_BUDGET = 15_000_000;
|
|
18
|
+
|
|
19
|
+
/** The configured budget: `CC_TOTAL_TOKENS_BUDGET` when it is a positive number, else Claude Code's default. */
|
|
20
|
+
export function turnTokenBudget(env: Record<string, string | undefined> = process.env): number {
|
|
21
|
+
const raw = env.CC_TOTAL_TOKENS_BUDGET;
|
|
22
|
+
const n = raw === undefined || raw.trim() === "" ? Number.NaN : Number(raw);
|
|
23
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : DEFAULT_TURN_TOKEN_BUDGET;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function finite(n: number | null | undefined): number | undefined {
|
|
27
|
+
return typeof n === "number" && Number.isFinite(n) ? n : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One turn's countdown. `startTurn` takes the context size the turn begins
|
|
32
|
+
* from; `left` takes the size now and returns what remains of the budget. The
|
|
33
|
+
* value never rises within a turn (as CC's does not), the first known size
|
|
34
|
+
* becomes the baseline when the turn started without an estimate, and an
|
|
35
|
+
* unknown size repeats the last value.
|
|
36
|
+
*/
|
|
37
|
+
export class TurnBudget {
|
|
38
|
+
private start: number | undefined;
|
|
39
|
+
private spent = 0;
|
|
40
|
+
|
|
41
|
+
constructor(readonly budget: number) {}
|
|
42
|
+
|
|
43
|
+
startTurn(contextTokens: number | null | undefined): void {
|
|
44
|
+
this.start = finite(contextTokens);
|
|
45
|
+
this.spent = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
left(contextTokens: number | null | undefined): number {
|
|
49
|
+
const now = finite(contextTokens);
|
|
50
|
+
if (now !== undefined) {
|
|
51
|
+
if (this.start === undefined) this.start = now;
|
|
52
|
+
this.spent = Math.max(this.spent, now - this.start);
|
|
53
|
+
}
|
|
54
|
+
return Math.max(0, this.budget - this.spent);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The bare block Claude Code appends, byte-for-byte. */
|
|
59
|
+
export function totalTokensBlock(left: number): string {
|
|
60
|
+
return `<total_tokens>${left} tokens left</total_tokens>`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-budget extension — Claude Code's `<total_tokens>N tokens left</total_tokens>`
|
|
3
|
+
* line, in the three places CC puts it (budget.ts says what the number is):
|
|
4
|
+
*
|
|
5
|
+
* - the end of every user message: the constant budget, a raw `sticky-append`
|
|
6
|
+
* reminder keyed once per session (`since: 0`, so a resumed session's earlier
|
|
7
|
+
* user messages carry it too — byte-identical to what they carried before);
|
|
8
|
+
* - every tool result: the countdown, a RAW `last-append` one-shot queued at
|
|
9
|
+
* `tool_result` time and written into the stored result by system-reminder
|
|
10
|
+
* (so it persists in the session like CC's and no later request re-caches the
|
|
11
|
+
* message). That is why this extension sits BEFORE `system-reminder` in the
|
|
12
|
+
* package manifest — pi runs `tool_result` handlers in load order, and the
|
|
13
|
+
* block must be queued before system-reminder's handler takes the one-shots.
|
|
14
|
+
* It emits only at runtime, never at load, so the "listeners load first" rule
|
|
15
|
+
* for the bus is not at stake;
|
|
16
|
+
* - the system prompt, before the gitStatus block — appended by the
|
|
17
|
+
* system-prompt extension from the same pure module.
|
|
18
|
+
*
|
|
19
|
+
* A turn starts at the first `agent_start` after a settle (whoever opened it: a
|
|
20
|
+
* prompt or a harness notification) and its baseline is pi's context estimate
|
|
21
|
+
* at that moment. CC_TOTAL_TOKENS=0 turns all three off.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { REMINDER_CHANNEL, type ReminderPayload } from "../lib/reminders.ts";
|
|
26
|
+
import { totalTokensBlock, TurnBudget, turnTokenBudget } from "./budget.ts";
|
|
27
|
+
|
|
28
|
+
const USER_MESSAGE_KEY = "total-tokens";
|
|
29
|
+
|
|
30
|
+
export default function contextBudgetExtension(pi: ExtensionAPI) {
|
|
31
|
+
const enabled = () => process.env.CC_TOTAL_TOKENS !== "0";
|
|
32
|
+
const budget = new TurnBudget(turnTokenBudget());
|
|
33
|
+
/** True between the first agent_start of a turn and agent_settled. */
|
|
34
|
+
let busy = false;
|
|
35
|
+
|
|
36
|
+
pi.on("session_start", () => {
|
|
37
|
+
busy = false;
|
|
38
|
+
if (!enabled()) return;
|
|
39
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
40
|
+
text: totalTokensBlock(budget.budget),
|
|
41
|
+
scope: "every-turn",
|
|
42
|
+
key: USER_MESSAGE_KEY,
|
|
43
|
+
placement: "sticky-append",
|
|
44
|
+
raw: true,
|
|
45
|
+
since: 0,
|
|
46
|
+
} satisfies ReminderPayload);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
50
|
+
if (busy) return;
|
|
51
|
+
busy = true;
|
|
52
|
+
budget.startTurn(ctx.getContextUsage()?.tokens);
|
|
53
|
+
});
|
|
54
|
+
pi.on("agent_settled", () => {
|
|
55
|
+
busy = false;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
pi.on("tool_result", (_event, ctx) => {
|
|
59
|
+
if (!enabled()) return;
|
|
60
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
61
|
+
text: totalTokensBlock(budget.left(ctx.getContextUsage()?.tokens)),
|
|
62
|
+
placement: "last-append",
|
|
63
|
+
raw: true,
|
|
64
|
+
} satisfies ReminderPayload);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* context-management extension — asks Anthropic to
|
|
2
|
+
* context-management extension — asks Anthropic to KEEP old thinking blocks.
|
|
3
3
|
*
|
|
4
4
|
* Claude Code sends `context_management: { edits: [{ type: "clear_thinking_20251015",
|
|
5
|
-
* keep: "all" }] }` on every request
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* keep: "all" }] }` on every request. Despite the edit's name, `keep: "all"` is
|
|
6
|
+
* a cache preserver, not a trimmer: models that strip previous-turn thinking by
|
|
7
|
+
* default (Haiku through 4.5, Sonnet <= 4.5, Opus <= 4.1) would otherwise
|
|
8
|
+
* invalidate the messages cache at the first stripped block; with the edit the
|
|
9
|
+
* API keeps every block and the cached prefix survives (Anthropic's docs: "keeps
|
|
10
|
+
* all thinking blocks (maximizes cache hits)"). It does not reduce context. pi
|
|
11
|
+
* does not send it, so a long One Code session on such a model would re-cache
|
|
12
|
+
* its history every turn without this extension — do not "optimize" the
|
|
13
|
+
* parameter away (findings §9, CACHE-REVIEW-2026-09-04 L1).
|
|
8
14
|
*
|
|
9
15
|
* Two things the parameter needs, both found empirically against the live API
|
|
10
16
|
* (2026-08-05, api.anthropic.com):
|
|
@@ -14,9 +20,11 @@
|
|
|
14
20
|
* Setting the header from `before_provider_headers` REPLACES the value pi
|
|
15
21
|
* computes at client creation (extension headers merge last), so we must
|
|
16
22
|
* rebuild pi's own beta list and append ours — see anthropicBetas(). That
|
|
17
|
-
* replicates pi-ai's createClient logic
|
|
18
|
-
*
|
|
19
|
-
*
|
|
23
|
+
* replicates pi-ai's createClient logic: OAuth identity betas, fine-grained
|
|
24
|
+
* tool streaming for models without eager input streaming, interleaved
|
|
25
|
+
* thinking for non-adaptive models, and server-side fallback for models with
|
|
26
|
+
* `compat.allowedFallbackModels` (pi also puts a `fallbacks` body field on
|
|
27
|
+
* those, which 400s if this beta is missing). Re-check on pi upgrades.
|
|
20
28
|
*
|
|
21
29
|
* - `thinking` enabled or an adaptive-thinking model, or the API 400s with
|
|
22
30
|
* "`clear_thinking_20251015` strategy requires `thinking` to be enabled or
|
|
@@ -33,6 +41,8 @@
|
|
|
33
41
|
import { readFileSync, statSync } from "node:fs";
|
|
34
42
|
import { join } from "node:path";
|
|
35
43
|
import { type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
44
|
+
import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
|
|
45
|
+
import { CACHE_RETENTION_ENV, mainSessionCacheRetention } from "../lib/cache-retention.ts";
|
|
36
46
|
|
|
37
47
|
const CLEAR_THINKING_EDIT = { type: "clear_thinking_20251015", keep: "all" };
|
|
38
48
|
const CONTEXT_MANAGEMENT_BETA = "context-management-2025-06-27";
|
|
@@ -42,10 +52,18 @@ const CONTEXT_MANAGEMENT_BETA = "context-management-2025-06-27";
|
|
|
42
52
|
const OAUTH_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
|
|
43
53
|
const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
|
|
44
54
|
const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
|
|
55
|
+
// Newer pi versions add a `fallbacks` body field for models whose compat
|
|
56
|
+
// carries `allowedFallbackModels` (server-side fallback, e.g. opus-5 →
|
|
57
|
+
// opus-4-8), gated on this beta. Because we overwrite the whole header, we must
|
|
58
|
+
// re-add the beta whenever pi added the body field, or the API 400s with
|
|
59
|
+
// "fallbacks: Extra inputs are not permitted" (verified live, api.anthropic.com
|
|
60
|
+
// 2026-09-01). Mirrors pi's shouldUseServerSideFallbackBeta.
|
|
61
|
+
const SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
|
|
45
62
|
|
|
46
63
|
export interface AnthropicModelCompat {
|
|
47
64
|
forceAdaptiveThinking?: boolean;
|
|
48
65
|
supportsEagerToolInputStreaming?: boolean;
|
|
66
|
+
allowedFallbackModels?: unknown[];
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
/** pi's beta list for this model/auth, with the context-management beta appended. */
|
|
@@ -55,19 +73,15 @@ export function anthropicBetas(oauth: boolean, compat: AnthropicModelCompat | un
|
|
|
55
73
|
// pi sends this only when tools are present; we always register tools.
|
|
56
74
|
if (compat?.supportsEagerToolInputStreaming === false) betas.push(FINE_GRAINED_TOOL_STREAMING_BETA);
|
|
57
75
|
if (compat?.forceAdaptiveThinking !== true) betas.push(INTERLEAVED_THINKING_BETA);
|
|
76
|
+
// pi puts a `fallbacks` body field on these models; the beta must ride along.
|
|
77
|
+
if ((compat?.allowedFallbackModels?.length ?? 0) > 0) betas.push(SERVER_SIDE_FALLBACK_BETA);
|
|
58
78
|
betas.push(CONTEXT_MANAGEMENT_BETA);
|
|
59
79
|
return betas.join(",");
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const record = payload as Record<string, unknown>;
|
|
66
|
-
if (!Array.isArray(record.messages)) return false;
|
|
67
|
-
if ("input" in record) return false;
|
|
68
|
-
const model = typeof record.model === "string" ? record.model : "";
|
|
69
|
-
return model.includes("claude") || typeof record.max_tokens === "number";
|
|
70
|
-
}
|
|
82
|
+
// The payload predicate lives in lib (pure, shared with compaction and
|
|
83
|
+
// tool-search); re-exported so existing importers keep working.
|
|
84
|
+
export { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
|
|
71
85
|
|
|
72
86
|
/** The edit is rejected unless thinking is enabled or the model is adaptive. */
|
|
73
87
|
export function clearThinkingApplies(payload: Record<string, unknown>, forceAdaptiveThinking: boolean): boolean {
|
|
@@ -130,6 +144,16 @@ export function clearThinkingEnabled(flag: string | undefined, model: Anthropici
|
|
|
130
144
|
export default function contextManagementExtension(pi: ExtensionAPI) {
|
|
131
145
|
const flag = () => process.env.CC_CLEAR_THINKING;
|
|
132
146
|
|
|
147
|
+
// Prompt-cache TTL for the main session: one hour in the interactive modes,
|
|
148
|
+
// the way Claude Code caches (a pause between turns no longer re-writes the
|
|
149
|
+
// context); a `-p`/json run keeps pi's 5-minute default, and so do
|
|
150
|
+
// in-process children (lib/cache-retention.ts). pi reads the variable per
|
|
151
|
+
// request, so setting it here, before the first prompt, is early enough.
|
|
152
|
+
pi.on("session_start", (_event, ctx) => {
|
|
153
|
+
const value = mainSessionCacheRetention(ctx.mode, process.env[CACHE_RETENTION_ENV]);
|
|
154
|
+
if (value) process.env[CACHE_RETENTION_ENV] = value;
|
|
155
|
+
});
|
|
156
|
+
|
|
133
157
|
pi.on("before_provider_headers", (event, ctx) => {
|
|
134
158
|
const model = ctx.model as AnthropicishModel | undefined;
|
|
135
159
|
if (!clearThinkingEnabled(flag(), model)) return;
|