@hassangameryt/freeflow 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/NOTICE +14 -0
- package/README.md +26 -31
- package/capabilities/cognitive-routing/SKILL.md +106 -161
- package/capabilities/cognitive-routing/references/both-mode.md +36 -0
- package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
- package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
- package/capabilities/compaction/SKILL.md +49 -0
- package/capabilities/compaction/references/summary-format.md +239 -0
- package/command-surface.json +4 -0
- package/gemini-extension.json +1 -1
- package/hooks/shared/runtime-context.mjs +28 -30
- package/licenses/Apache-2.0.txt +55 -0
- package/package.json +22 -9
- package/pi-extension/README.md +41 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
- package/pi-extension/dist/cognitive-routing/economics.js +45 -0
- package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
- package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
- package/pi-extension/dist/cognitive-routing/gate.js +241 -0
- package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
- package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
- package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
- package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
- package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
- package/pi-extension/dist/cognitive-routing/session.js +221 -0
- package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
- package/pi-extension/dist/cognitive-routing/status.js +80 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
- package/pi-extension/dist/compaction/carry.js +191 -0
- package/pi-extension/dist/compaction/config.js +22 -0
- package/pi-extension/dist/compaction/controller.js +471 -0
- package/pi-extension/dist/compaction/harness.js +77 -0
- package/pi-extension/dist/compaction/results.js +158 -0
- package/pi-extension/dist/compaction/thresholds.js +35 -0
- package/pi-extension/dist/compaction/tool.js +57 -0
- package/pi-extension/dist/host/branch.js +13 -0
- package/pi-extension/dist/host/canonical.js +12 -0
- package/pi-extension/dist/host/catalog.js +77 -0
- package/pi-extension/dist/host/config.js +374 -0
- package/pi-extension/dist/host/projection-tags.js +60 -0
- package/pi-extension/dist/host/prompts.js +82 -0
- package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
- package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
- package/pi-extension/dist/host/runtime-state.js +189 -0
- package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
- package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
- package/pi-extension/dist/host/staging.js +28 -0
- package/pi-extension/dist/host/status.js +101 -0
- package/pi-extension/dist/index.js +423 -173
- package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
- package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
- package/pi-extension/dist/provider-support/cache/health.js +73 -0
- package/pi-extension/dist/provider-support/cache/index.js +58 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
- package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
- package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
- package/pi-extension/dist/provider-support/effort.js +13 -0
- package/pi-extension/dist/provider-support/index.js +44 -3
- package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
- package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
- package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
- package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
- package/pi-extension/dist/provider-support/routes.js +17 -0
- package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
- package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
- package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
- package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
- package/pi-extension/dist/tool-execution/background.js +238 -0
- package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
- package/pi-extension/dist/tool-execution/config.js +22 -0
- package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
- package/pi-extension/dist/tool-execution/file-state.js +138 -0
- package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
- package/pi-extension/dist/tool-execution/prompt.js +54 -0
- package/pi-extension/dist/tool-execution/tools.js +16 -0
- package/pi-extension/freeflow/README.md +9 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +22 -75
- package/runtime/prompts/interaction-contract.md +15 -8
- package/runtime/prompts/tool-execution.md +38 -0
- package/runtime/prompts/working-method.md +108 -0
- package/skills/action-selection/SKILL.md +39 -47
- package/skills/action-selection/references/trajectory-stalls.md +2 -0
- package/skills/bypass/SKILL.md +2 -2
- package/skills/commit-work/SKILL.md +31 -83
- package/skills/commit-work/references/git-state-edges.md +1 -1
- package/skills/commit-work/references/simple-push.md +2 -0
- package/skills/commit-work/references/staging-decisions.md +1 -1
- package/skills/decision-gate/SKILL.md +31 -50
- package/skills/design-for-depth/SKILL.md +44 -53
- package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
- package/skills/design-for-depth/references/implementation-decisions.md +1 -1
- package/skills/design-for-depth/references/interface-design-loop.md +5 -36
- package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
- package/skills/diagnose-failure/SKILL.md +39 -24
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
- package/skills/discuss/SKILL.md +35 -11
- package/skills/discuss/references/discussion-continuity.md +7 -27
- package/skills/evaluate-skill/SKILL.md +49 -80
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
- package/skills/evaluate-skill/references/evaluation-design.md +14 -14
- package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
- package/skills/evaluate-skill/references/review-and-revision.md +3 -1
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
- package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
- package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
- package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
- package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
- package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
- package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
- package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
- package/skills/execute-work/SKILL.md +41 -9
- package/skills/execute-work/references/code-practices.md +2 -0
- package/skills/execute-work/references/execute-work-edges.md +11 -1
- package/skills/handoff/SKILL.md +1 -1
- package/skills/handoff/references/handoff-templates.md +1 -1
- package/skills/review-artifact/SKILL.md +42 -41
- package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +38 -32
- package/skills/review-work/references/adjudicate-work-review.md +1 -1
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/review-work/references/security-risk-lens.md +2 -4
- package/skills/setup-freeflow/SKILL.md +3 -1
- package/skills/setup-freeflow/references/activation-contract.md +7 -13
- package/skills/setup-freeflow/references/host-setup.md +3 -3
- package/skills/track-work/SKILL.md +113 -161
- package/skills/track-work/references/working-record-format.md +189 -350
- package/skills/track-work/scripts/lib/document.mjs +62 -6
- package/skills/track-work/scripts/lib/format.mjs +16 -1
- package/skills/track-work/scripts/lib/store.mjs +9 -8
- package/skills/track-work/scripts/lib/transitions.mjs +79 -4
- package/skills/track-work/scripts/working-record.mjs +34 -12
- package/skills/verify-work/SKILL.md +29 -20
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
- package/skills/verify-work/references/integration-evidence.md +2 -0
- package/skills/verify-work/references/performance-evidence.md +3 -1
- package/skills/verify-work/references/test-design.md +3 -1
- package/skills/workflow/SKILL.md +65 -21
- package/skills/workflow/references/domain-skill-composition.md +5 -3
- package/skills/write-docs/SKILL.md +83 -0
- package/skills/write-docs/references/documentation-shapes.md +119 -0
- package/skills/write-docs/scripts/docs-check.mjs +139 -0
- package/skills/write-plan/SKILL.md +40 -40
- package/skills/write-plan/references/plan-shapes.md +1 -1
- package/skills/write-skill/SKILL.md +107 -147
- package/skills/write-skill/references/activation-boundaries.md +2 -0
- package/skills/write-skill/references/agent-first-instructions.md +22 -10
- package/skills/write-skill/references/progressive-disclosure.md +4 -2
- package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
- package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
- package/skills/write-skill/scripts/skill-author.mjs +18 -2
- package/skills/write-spec/SKILL.md +34 -41
- package/skills/write-spec/references/artifact-standards.md +1 -1
- package/skills/write-spec/references/decision-records.md +2 -0
- package/skills/write-spec/references/spec-shapes.md +1 -1
- package/capabilities/context-virtualization/SKILL.md +0 -103
- package/capabilities/conversation-history/SKILL.md +0 -53
- package/hooks/tests/check-host-adapters.test.mjs +0 -82
- package/hooks/tests/check-runtime-context-hook.sh +0 -212
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
- package/pi-extension/dist/context-virtualization/commands.js +0 -89
- package/pi-extension/dist/context-virtualization/projector.js +0 -42
- package/pi-extension/dist/context-virtualization/runtime.js +0 -378
- package/pi-extension/dist/context-virtualization/state.js +0 -57
- package/pi-extension/dist/context-virtualization/types.js +0 -10
- package/pi-extension/dist/conversation-history/passages.js +0 -66
- package/pi-extension/dist/conversation-history/ranking.js +0 -105
- package/pi-extension/dist/conversation-history/runtime.js +0 -226
- package/pi-extension/dist/conversation-history/search.js +0 -56
- package/pi-extension/dist/conversation-history/types.js +0 -1
- package/pi-extension/dist/freeflow-context/resolver.js +0 -120
- package/pi-extension/dist/freeflow-context/runtime.js +0 -80
- package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
- package/pi-extension/dist/freeflow-context/tool.js +0 -314
- package/pi-extension/dist/freeflow-context/types.js +0 -12
- package/pi-extension/dist/freeflow-context/visibility.js +0 -28
- package/pi-extension/dist/integrations/mcp-config.js +0 -29
- package/pi-extension/dist/runtime/runtime-context.js +0 -754
- package/pi-extension/dist/runtime/runtime-identity.js +0 -5
- package/pi-extension/dist/session-sources/provenance.js +0 -32
- package/pi-extension/dist/ui/text-component.js +0 -57
- package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
- package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
- package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
- package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
- package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
- package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
- package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
- package/pi-extension/src/context-virtualization/commands.ts +0 -111
- package/pi-extension/src/context-virtualization/projector.ts +0 -62
- package/pi-extension/src/context-virtualization/runtime.ts +0 -428
- package/pi-extension/src/context-virtualization/state.ts +0 -79
- package/pi-extension/src/context-virtualization/types.ts +0 -92
- package/pi-extension/src/conversation-history/passages.ts +0 -81
- package/pi-extension/src/conversation-history/ranking.ts +0 -155
- package/pi-extension/src/conversation-history/runtime.ts +0 -250
- package/pi-extension/src/conversation-history/search.ts +0 -79
- package/pi-extension/src/conversation-history/types.ts +0 -31
- package/pi-extension/src/freeflow-context/resolver.ts +0 -139
- package/pi-extension/src/freeflow-context/runtime.ts +0 -100
- package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
- package/pi-extension/src/freeflow-context/tool.ts +0 -366
- package/pi-extension/src/freeflow-context/types.ts +0 -70
- package/pi-extension/src/freeflow-context/visibility.ts +0 -53
- package/pi-extension/src/index.ts +0 -390
- package/pi-extension/src/integrations/mcp-config.ts +0 -32
- package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
- package/pi-extension/src/provider-support/astra/history.ts +0 -145
- package/pi-extension/src/provider-support/astra/index.ts +0 -12
- package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
- package/pi-extension/src/provider-support/index.ts +0 -5
- package/pi-extension/src/runtime/request-history.ts +0 -243
- package/pi-extension/src/runtime/runtime-context.ts +0 -860
- package/pi-extension/src/runtime/runtime-identity.ts +0 -8
- package/pi-extension/src/session-sources/events.ts +0 -230
- package/pi-extension/src/session-sources/provenance.ts +0 -39
- package/pi-extension/src/session-sources/read-only-session.ts +0 -170
- package/pi-extension/src/session-sources/sources.ts +0 -334
- package/pi-extension/src/settings/settings-tui.ts +0 -501
- package/pi-extension/src/settings/settings-ui.ts +0 -1652
- package/pi-extension/src/ui/text-component.ts +0 -66
- package/pi-extension/tests/cache-reuse/history.test.js +0 -117
- package/pi-extension/tests/cache-reuse/native.test.js +0 -91
- package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
- package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
- package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
- package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
- package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
- package/pi-extension/tests/conversation-history/search.test.js +0 -97
- package/pi-extension/tests/fixtures/pi-host.js +0 -4
- package/pi-extension/tests/fixtures/routing-native.js +0 -191
- package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
- package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
- package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
- package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
- package/pi-extension/tests/provider-support/astra.test.js +0 -306
- package/pi-extension/tests/provider-support/cli.test.js +0 -92
- package/pi-extension/tests/provider-support/native.test.js +0 -183
- package/pi-extension/tests/settings/settings-tui.test.js +0 -243
- package/pi-extension/tsconfig.json +0 -16
- package/runtime/prompts/context-virtualization.md +0 -3
- package/runtime/prompts/conversation-history.md +0 -3
- package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep a remembered cache entry reachable. A provider only reads an entry that lies within its
|
|
3
|
+
* lookback window of some breakpoint, so when a request extends an earlier one by more than that
|
|
4
|
+
* window (for example after another model's turns were inserted), the earlier entry is lost even
|
|
5
|
+
* though the prefix is unchanged. Placing a breakpoint on the earlier request's final entry lets
|
|
6
|
+
* the request read it, and only the new tail is written.
|
|
7
|
+
*/
|
|
8
|
+
export function planAnchor(prior, current, limits) {
|
|
9
|
+
let entry;
|
|
10
|
+
for (const layout of prior) {
|
|
11
|
+
const end = layout.breakpoints.at(-1);
|
|
12
|
+
if (end === undefined || end >= current.chain.length || layout.chain[end] !== current.chain[end]) continue;
|
|
13
|
+
if (entry === undefined || end > entry) entry = end;
|
|
14
|
+
}
|
|
15
|
+
if (entry === undefined || current.breakpoints.includes(entry)) return;
|
|
16
|
+
const reach = current.breakpoints.find((position) => position > entry);
|
|
17
|
+
if (reach === undefined || reach - entry <= limits.lookback - limits.margin) return;
|
|
18
|
+
if (current.breakpoints.length < limits.maxBreakpoints) return { add: entry };
|
|
19
|
+
const remove = current.yieldable.find((position) => position < entry && current.breakpoints.includes(position));
|
|
20
|
+
return remove === undefined ? undefined : { add: entry, remove };
|
|
21
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Anthropic Messages: four explicit breakpoints, each reading entries up to 20 positions back. */
|
|
3
|
+
export const ANTHROPIC_LIMITS = { maxBreakpoints: 4, lookback: 20, margin: 5 };
|
|
4
|
+
// Consecutive tool_use blocks, and consecutive tool_result blocks, form one lookback position.
|
|
5
|
+
const RUN_TYPES = new Set(["tool_use", "tool_result"]);
|
|
6
|
+
// Thinking blocks cannot carry cache_control.
|
|
7
|
+
const UNMARKABLE = new Set(["thinking", "redacted_thinking"]);
|
|
8
|
+
const withoutMarkers = (value) => JSON.stringify(value, (key, v) => (key === "cache_control" ? undefined : v));
|
|
9
|
+
export function anthropicLayout(payload) {
|
|
10
|
+
if (!payload || !Array.isArray(payload.messages)) return;
|
|
11
|
+
const chain = [],
|
|
12
|
+
handles = [],
|
|
13
|
+
breakpoints = [];
|
|
14
|
+
let hash = "",
|
|
15
|
+
pending = "";
|
|
16
|
+
const push = (fingerprint, handle, marked) => {
|
|
17
|
+
hash = createHash("sha256").update(hash).update(pending).update(fingerprint).digest("hex");
|
|
18
|
+
pending = "";
|
|
19
|
+
if (marked) breakpoints.push(chain.length);
|
|
20
|
+
chain.push(hash);
|
|
21
|
+
handles.push(handle);
|
|
22
|
+
};
|
|
23
|
+
if (Array.isArray(payload.tools) && payload.tools.length)
|
|
24
|
+
push(
|
|
25
|
+
`tools:${withoutMarkers(payload.tools)}`,
|
|
26
|
+
{ kind: "tools" },
|
|
27
|
+
payload.tools.some((t) => t?.cache_control),
|
|
28
|
+
);
|
|
29
|
+
const system = typeof payload.system === "string" ? [{ type: "text", text: payload.system }] : (payload.system ?? []);
|
|
30
|
+
system.forEach((block, index) =>
|
|
31
|
+
push(
|
|
32
|
+
`system:${withoutMarkers(block)}`,
|
|
33
|
+
typeof payload.system === "string" ? undefined : { kind: "system", index },
|
|
34
|
+
!!block?.cache_control,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
payload.messages.forEach((message, m) => {
|
|
38
|
+
const start = `message:${message.role}:`;
|
|
39
|
+
if (!Array.isArray(message.content)) {
|
|
40
|
+
// Unmarkable without reshaping the message, but still part of the prefix.
|
|
41
|
+
push(`${start}${withoutMarkers(message.content)}`, undefined, false);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!message.content.length) {
|
|
45
|
+
// Content-less messages (such as per-message effort) change the prefix without adding a position.
|
|
46
|
+
pending += `${start}${withoutMarkers(message)}`;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Mid-conversation role:"system" updates carry markers too; Pi puts the final one there when an update ends the request.
|
|
50
|
+
const markable = message.role === "user" || message.role === "assistant" || message.role === "system";
|
|
51
|
+
let b = 0;
|
|
52
|
+
while (b < message.content.length) {
|
|
53
|
+
const type = message.content[b]?.type;
|
|
54
|
+
let end = b;
|
|
55
|
+
if (RUN_TYPES.has(type)) while (message.content[end + 1]?.type === type) end++;
|
|
56
|
+
const run = message.content.slice(b, end + 1);
|
|
57
|
+
push(
|
|
58
|
+
`${b === 0 ? start : ""}${withoutMarkers(run)}`,
|
|
59
|
+
markable && !UNMARKABLE.has(message.content[end]?.type) ? { kind: "block", message: m, block: end } : undefined,
|
|
60
|
+
run.some((block) => block?.cache_control),
|
|
61
|
+
);
|
|
62
|
+
b = end + 1;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (pending) push("", undefined, false);
|
|
66
|
+
const systemMarks = breakpoints.filter((p) => handles[p]?.kind === "system");
|
|
67
|
+
const yieldable = [
|
|
68
|
+
...breakpoints.filter((p) => handles[p]?.kind === "tools"),
|
|
69
|
+
// The last system breakpoint covers every earlier one while the system prompt is unchanged.
|
|
70
|
+
...systemMarks.slice(0, -1),
|
|
71
|
+
];
|
|
72
|
+
return { layout: { chain, breakpoints, yieldable }, handles };
|
|
73
|
+
}
|
|
74
|
+
function markerAt(payload, handle) {
|
|
75
|
+
if (handle?.kind === "tools") return payload.tools.findLast((t) => t.cache_control)?.cache_control;
|
|
76
|
+
if (handle?.kind === "system") return payload.system[handle.index].cache_control;
|
|
77
|
+
if (handle?.kind === "block") return payload.messages[handle.message].content[handle.block].cache_control;
|
|
78
|
+
}
|
|
79
|
+
/** Apply a plan without mutating the host payload: only the touched containers are copied. */
|
|
80
|
+
export function applyAnthropicAnchor(payload, { layout, handles }, plan) {
|
|
81
|
+
const add = handles[plan.add];
|
|
82
|
+
// Reuse the final breakpoint's marker so TTL ordering (longer lifetimes first) is preserved.
|
|
83
|
+
const marker = markerAt(payload, handles[layout.breakpoints.at(-1)]);
|
|
84
|
+
if (add?.kind !== "block" || !marker) return payload;
|
|
85
|
+
const next = { ...payload, messages: [...payload.messages] };
|
|
86
|
+
const message = { ...next.messages[add.message], content: [...next.messages[add.message].content] };
|
|
87
|
+
message.content[add.block] = { ...message.content[add.block], cache_control: marker };
|
|
88
|
+
next.messages[add.message] = message;
|
|
89
|
+
const remove = plan.remove === undefined ? undefined : handles[plan.remove];
|
|
90
|
+
if (remove?.kind === "tools")
|
|
91
|
+
next.tools = payload.tools.map((tool) => {
|
|
92
|
+
if (!tool?.cache_control) return tool;
|
|
93
|
+
const { cache_control: _marker, ...rest } = tool;
|
|
94
|
+
return rest;
|
|
95
|
+
});
|
|
96
|
+
else if (remove?.kind === "system") {
|
|
97
|
+
next.system = [...payload.system];
|
|
98
|
+
const { cache_control: _marker, ...rest } = next.system[remove.index];
|
|
99
|
+
next.system[remove.index] = rest;
|
|
100
|
+
}
|
|
101
|
+
return next;
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const WINDOW = 8;
|
|
2
|
+
const MISSES = 4;
|
|
3
|
+
const GAP_MS = 5 * 60_000;
|
|
4
|
+
const MIN_PROMPT = 8_000;
|
|
5
|
+
const HIT_SHARE = 0.5;
|
|
6
|
+
const tokens = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : `${Math.round(n / 1e3)}k`);
|
|
7
|
+
/**
|
|
8
|
+
* Detects prompt-cache regressions: back-to-back requests to the same model and effort, with nothing in between
|
|
9
|
+
* that legitimately invalidates the cache, should reuse the previous prompt. Provider-side misses are uncommon
|
|
10
|
+
* (about 2-4% of Codex requests in September 2026 sessions, with or without Freeflow), so repeated misses in that
|
|
11
|
+
* situation mean something is rewriting the start of the prompt.
|
|
12
|
+
* A routing profile that hands off and later resumes on the same model should likewise reuse its own
|
|
13
|
+
* previous prompt while that prompt's cache entry lives.
|
|
14
|
+
*/
|
|
15
|
+
export class CacheHealth {
|
|
16
|
+
previous;
|
|
17
|
+
lanes = new Map();
|
|
18
|
+
windows = new Map();
|
|
19
|
+
warned = new Map();
|
|
20
|
+
/** Compaction, navigation, or reload legitimately invalidates every cached prompt. */
|
|
21
|
+
noteBreak() {
|
|
22
|
+
this.previous = undefined;
|
|
23
|
+
this.lanes.clear();
|
|
24
|
+
}
|
|
25
|
+
/** A model or effort change starts a new back-to-back run; a routing lane keeps its own prompt. */
|
|
26
|
+
noteSwitch() {
|
|
27
|
+
this.previous = undefined;
|
|
28
|
+
}
|
|
29
|
+
reset() {
|
|
30
|
+
this.previous = undefined;
|
|
31
|
+
this.lanes.clear();
|
|
32
|
+
this.windows.clear();
|
|
33
|
+
this.warned.clear();
|
|
34
|
+
}
|
|
35
|
+
/** Returns a warning the first time a model's recent comparable requests cross the miss threshold. */
|
|
36
|
+
observe(observation) {
|
|
37
|
+
const key = `${observation.provider ?? "unknown"}/${observation.model ?? "unknown"}/${observation.thinking ?? "unknown"}`;
|
|
38
|
+
const prompt = observation.input + observation.cacheRead + (observation.cacheWrite ?? 0);
|
|
39
|
+
const previous = this.previous;
|
|
40
|
+
this.previous = { key, lane: observation.lane, at: observation.at, prompt };
|
|
41
|
+
let basis;
|
|
42
|
+
// Routing profiles see different views of the conversation, so one never reuses another's prompt.
|
|
43
|
+
if (
|
|
44
|
+
previous &&
|
|
45
|
+
previous.key === key &&
|
|
46
|
+
previous.lane === observation.lane &&
|
|
47
|
+
observation.at - previous.at <= GAP_MS
|
|
48
|
+
)
|
|
49
|
+
basis = previous;
|
|
50
|
+
else if (observation.lane) {
|
|
51
|
+
const own = this.lanes.get(`${key}/${observation.lane}`);
|
|
52
|
+
if (own && observation.at - own.at <= (observation.ttlMs ?? GAP_MS)) basis = own;
|
|
53
|
+
}
|
|
54
|
+
if (observation.lane) this.lanes.set(`${key}/${observation.lane}`, { at: observation.at, prompt });
|
|
55
|
+
if (!basis || basis.prompt < MIN_PROMPT) return;
|
|
56
|
+
const miss = observation.cacheRead < HIT_SHARE * basis.prompt;
|
|
57
|
+
const window = [
|
|
58
|
+
...(this.windows.get(key) ?? []),
|
|
59
|
+
{ miss, lost: miss ? basis.prompt - observation.cacheRead : 0 },
|
|
60
|
+
].slice(-WINDOW);
|
|
61
|
+
this.windows.set(key, window);
|
|
62
|
+
const misses = window.filter((entry) => entry.miss).length;
|
|
63
|
+
if (misses <= 1) this.warned.delete(key);
|
|
64
|
+
if (misses < MISSES || this.warned.has(key)) return;
|
|
65
|
+
const lost = window.reduce((sum, entry) => sum + entry.lost, 0);
|
|
66
|
+
const message = `Freeflow: the prompt cache for ${observation.model} (${observation.thinking}) missed on ${misses} of the last ${window.length} requests that should have reused it (~${tokens(lost)} tokens re-read). Something is rewriting the start of the prompt or placing it out of the provider's reach; check recently changed extensions or Freeflow settings.`;
|
|
67
|
+
this.warned.set(key, message);
|
|
68
|
+
return message;
|
|
69
|
+
}
|
|
70
|
+
status() {
|
|
71
|
+
return [...this.warned.values()];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isChatGPTSignIn } from "../routes.js";
|
|
2
|
+
import { planAnchor } from "./anchor.js";
|
|
3
|
+
import { ANTHROPIC_LIMITS, anthropicLayout, applyAnthropicAnchor } from "./anthropic.js";
|
|
4
|
+
import {
|
|
5
|
+
OPENAI_RESPONSES_LIMITS,
|
|
6
|
+
applyResponsesAnchor,
|
|
7
|
+
responsesLayout,
|
|
8
|
+
supportsExplicitBreakpoints,
|
|
9
|
+
} from "./openai-responses.js";
|
|
10
|
+
export { planAnchor } from "./anchor.js";
|
|
11
|
+
const responses = {
|
|
12
|
+
limits: OPENAI_RESPONSES_LIMITS,
|
|
13
|
+
applies: supportsExplicitBreakpoints,
|
|
14
|
+
layout: responsesLayout,
|
|
15
|
+
apply: applyResponsesAnchor,
|
|
16
|
+
};
|
|
17
|
+
// APIs with unbounded prefix lookup need no entry: every prefix they wrote stays reachable. The ChatGPT
|
|
18
|
+
// Codex backend is one: live probes found full prefix reuse after 60 appended message endings, and it
|
|
19
|
+
// rejects prompt_cache_breakpoint, so it must never be adapted. Sign in with ChatGPT on the openai provider
|
|
20
|
+
// reaches the same backend (full reuse after 25 endings, breakpoints rejected) and is skipped in adapt().
|
|
21
|
+
const PROTOCOLS = {
|
|
22
|
+
"anthropic-messages": { limits: ANTHROPIC_LIMITS, layout: anthropicLayout, apply: applyAnthropicAnchor },
|
|
23
|
+
"openai-responses": responses,
|
|
24
|
+
"azure-openai-responses": responses,
|
|
25
|
+
};
|
|
26
|
+
/** Recent requests per session and model; enough to find the requester's own earlier entry. */
|
|
27
|
+
const REMEMBERED = 8;
|
|
28
|
+
export class CacheAnchorAdapter {
|
|
29
|
+
enabled;
|
|
30
|
+
lanes = new Map();
|
|
31
|
+
constructor(enabled = () => true) {
|
|
32
|
+
this.enabled = enabled;
|
|
33
|
+
}
|
|
34
|
+
reset() {
|
|
35
|
+
this.lanes.clear();
|
|
36
|
+
}
|
|
37
|
+
adapt(payload, ctx) {
|
|
38
|
+
const model = ctx?.model;
|
|
39
|
+
const protocol = model && PROTOCOLS[model.api];
|
|
40
|
+
if (!protocol || (protocol.applies && !protocol.applies(model)) || !this.enabled()) return payload;
|
|
41
|
+
if (isChatGPTSignIn(model, ctx)) return payload;
|
|
42
|
+
try {
|
|
43
|
+
const session = ctx.sessionManager?.getSessionId?.() ?? "memory";
|
|
44
|
+
const lane = JSON.stringify([session, model.provider, model.api, model.id, model.baseUrl ?? ""]);
|
|
45
|
+
const mapped = protocol.layout(payload);
|
|
46
|
+
if (!mapped) return payload;
|
|
47
|
+
const prior = this.lanes.get(lane) ?? [];
|
|
48
|
+
const plan = planAnchor(prior, mapped.layout, protocol.limits);
|
|
49
|
+
const sent = plan ? protocol.apply(payload, mapped, plan) : payload;
|
|
50
|
+
const remembered = sent === payload ? mapped.layout : protocol.layout(sent).layout;
|
|
51
|
+
this.lanes.set(lane, [remembered, ...prior].slice(0, REMEMBERED));
|
|
52
|
+
return sent;
|
|
53
|
+
} catch {
|
|
54
|
+
// Cache placement is an optimization; never turn it into a failed request.
|
|
55
|
+
return payload;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { isChatGPTSignIn } from "../routes.js";
|
|
2
|
+
const responses = {
|
|
3
|
+
cap: (payload) => ({ ...payload, max_output_tokens: 16 }),
|
|
4
|
+
retention: (payload) =>
|
|
5
|
+
payload?.prompt_cache_retention === "24h" || payload?.prompt_cache_options?.ttl ? "long" : undefined,
|
|
6
|
+
};
|
|
7
|
+
const PROTOCOLS = {
|
|
8
|
+
"anthropic-messages": {
|
|
9
|
+
cap: (payload) => ({ ...payload, max_tokens: 1 }),
|
|
10
|
+
retention: (payload) => (JSON.stringify(payload).includes('"ttl":"1h"') ? "long" : "short"),
|
|
11
|
+
// Budget-based thinking must stay below max_tokens and is part of the cache key, so a one-token replay is
|
|
12
|
+
// rejected or writes a different entry. Adaptive thinking has no budget. Pi's own warmer skips these too.
|
|
13
|
+
replayable: (payload) => payload?.thinking?.type !== "enabled",
|
|
14
|
+
},
|
|
15
|
+
"openai-responses": responses,
|
|
16
|
+
"azure-openai-responses": responses,
|
|
17
|
+
};
|
|
18
|
+
/** Continuation is near-certain while a delegated worker runs: the requester resumes on its return. */
|
|
19
|
+
const CONTINUATION = 0.9;
|
|
20
|
+
/** Same threshold as Pi's own warmer: a refresh must be expected to save at least this much. */
|
|
21
|
+
const MINIMUM_SAVINGS = 0.05;
|
|
22
|
+
/** A refresh counts as reuse only when it reads nearly all of the prompt. */
|
|
23
|
+
const REUSE = 0.9;
|
|
24
|
+
const EXPIRY_MARGIN_MS = 10_000;
|
|
25
|
+
/**
|
|
26
|
+
* Keeps a suspended requester's prompt cache warm while it is certain to resume, such as a Coordinator
|
|
27
|
+
* whose delegated worker is still running. Pi's own warmer only keeps the latest request warm, which
|
|
28
|
+
* during a worker run belongs to the worker. Eligibility and economics come from the model's declared
|
|
29
|
+
* cache lifetime and prices, so any provider that declares them is handled the same way.
|
|
30
|
+
*/
|
|
31
|
+
export class CacheKeepAlive {
|
|
32
|
+
options;
|
|
33
|
+
lanes = new Map();
|
|
34
|
+
holdSource = () => undefined;
|
|
35
|
+
requester = () => undefined;
|
|
36
|
+
active;
|
|
37
|
+
lastStop;
|
|
38
|
+
now;
|
|
39
|
+
setTimer;
|
|
40
|
+
clearTimer;
|
|
41
|
+
send;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.now = options.now ?? Date.now;
|
|
45
|
+
this.setTimer = options.setTimer ?? ((fn, ms) => setTimeout(fn, ms).unref?.());
|
|
46
|
+
this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
47
|
+
this.send = options.send ?? sendThroughHost;
|
|
48
|
+
}
|
|
49
|
+
setHoldSource(source) {
|
|
50
|
+
this.holdSource = source;
|
|
51
|
+
}
|
|
52
|
+
/** Names who is sending the current request, so requesters sharing a model keep separate lanes. */
|
|
53
|
+
setRequesterSource(source) {
|
|
54
|
+
this.requester = () => {
|
|
55
|
+
try {
|
|
56
|
+
return source();
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
reset() {
|
|
63
|
+
this.stop("reset");
|
|
64
|
+
this.lanes.clear();
|
|
65
|
+
}
|
|
66
|
+
status() {
|
|
67
|
+
if (this.active)
|
|
68
|
+
return `Keeping ${this.lanes.get(this.active.lane)?.model?.id} cache warm while its worker runs · $${this.active.spent.toFixed(3)} spent`;
|
|
69
|
+
return this.lastStop;
|
|
70
|
+
}
|
|
71
|
+
/** Remember the request just sent on its model; a hold keeps the latest one warm. */
|
|
72
|
+
record(payload, ctx) {
|
|
73
|
+
const key = laneKey(ctx, undefined, this.requester());
|
|
74
|
+
// Sign in with ChatGPT rejects max_output_tokens (live, 400), so a replay cannot be capped and is never sent.
|
|
75
|
+
const protocol = isChatGPTSignIn(ctx.model, ctx) ? undefined : PROTOCOLS[ctx.model?.api];
|
|
76
|
+
if (!key || !protocol) return;
|
|
77
|
+
if (protocol.replayable?.(payload) === false) {
|
|
78
|
+
this.lanes.delete(key);
|
|
79
|
+
if (this.active?.lane === key) this.stop("this request cannot be replayed with a capped output");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const prior = this.lanes.get(key);
|
|
83
|
+
this.lanes.set(key, { key, model: ctx.model, payload, sentAt: this.now(), prompt: prior?.prompt });
|
|
84
|
+
if (this.active?.lane === key) this.schedule();
|
|
85
|
+
}
|
|
86
|
+
/** Provider-reported usage of the latest request on a model gives the prompt size to price. */
|
|
87
|
+
observe(ctx, usage, producer) {
|
|
88
|
+
const lane = this.lanes.get(laneKey(ctx, producer, this.requester()) ?? "");
|
|
89
|
+
if (lane && usage) lane.prompt = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
90
|
+
}
|
|
91
|
+
/** Start or stop holding according to the current run and hold source. */
|
|
92
|
+
evaluate(ctx) {
|
|
93
|
+
const hold = this.currentHold(ctx);
|
|
94
|
+
const key = hold && laneKey(ctx, hold, hold.requester);
|
|
95
|
+
if (this.active && this.active.lane === key) {
|
|
96
|
+
this.active.ctx = ctx;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.stop(hold ? "hold changed" : "no suspended requester");
|
|
100
|
+
if (!key || !this.lanes.has(key)) return;
|
|
101
|
+
this.active = { lane: key, since: this.now(), spent: 0, ctx };
|
|
102
|
+
this.schedule();
|
|
103
|
+
}
|
|
104
|
+
currentHold(ctx) {
|
|
105
|
+
if (!(this.options.enabled?.() ?? true) || ctx?.isIdle?.() !== false) return;
|
|
106
|
+
try {
|
|
107
|
+
return this.holdSource();
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
stop(reason) {
|
|
113
|
+
if (this.active?.timer !== undefined) this.clearTimer(this.active.timer);
|
|
114
|
+
if (this.active) this.lastStop = `Cache keep-alive stopped: ${reason}`;
|
|
115
|
+
this.active = undefined;
|
|
116
|
+
}
|
|
117
|
+
ttlMs(lane) {
|
|
118
|
+
const protocol = PROTOCOLS[lane.model.api];
|
|
119
|
+
const env = this.options.env ?? process.env;
|
|
120
|
+
const retention = protocol?.retention(lane.payload) ?? (env.PI_CACHE_RETENTION === "long" ? "long" : "short");
|
|
121
|
+
const seconds = lane.model.promptCache?.[retention];
|
|
122
|
+
return typeof seconds === "number" && seconds > 0 ? seconds * 1000 : undefined;
|
|
123
|
+
}
|
|
124
|
+
schedule() {
|
|
125
|
+
const active = this.active;
|
|
126
|
+
if (!active) return;
|
|
127
|
+
if (active.timer !== undefined) this.clearTimer(active.timer);
|
|
128
|
+
active.timer = undefined;
|
|
129
|
+
const lane = this.lanes.get(active.lane);
|
|
130
|
+
const ttl = this.ttlMs(lane);
|
|
131
|
+
if (!ttl || ttl <= EXPIRY_MARGIN_MS) return this.stop("model declares no cache lifetime");
|
|
132
|
+
const at = lane.sentAt + refreshDelay(ttl);
|
|
133
|
+
active.timer = this.setTimer(() => this.refresh(active), Math.max(0, at - this.now()));
|
|
134
|
+
}
|
|
135
|
+
async refresh(active) {
|
|
136
|
+
if (this.active !== active) return;
|
|
137
|
+
active.timer = undefined;
|
|
138
|
+
const lane = this.lanes.get(active.lane);
|
|
139
|
+
const hold = this.currentHold(active.ctx);
|
|
140
|
+
if (!hold || laneKey(active.ctx, hold, hold.requester) !== active.lane)
|
|
141
|
+
return this.stop("requester resumed or run ended");
|
|
142
|
+
const now = this.now();
|
|
143
|
+
// Like Pi's warmer, a refresh later than halfway from its slot to expiry is likely a full rewrite.
|
|
144
|
+
const ttl = this.ttlMs(lane),
|
|
145
|
+
delay = refreshDelay(ttl);
|
|
146
|
+
if (now > lane.sentAt + delay + (ttl - delay) / 2) return this.stop("cache entry already expired");
|
|
147
|
+
if (now - active.since > (this.options.maxHoldMs ?? 6 * 3_600_000)) return this.stop("hold duration limit");
|
|
148
|
+
const economics = price(lane);
|
|
149
|
+
if (!economics) return this.stop("prompt size or prices unknown");
|
|
150
|
+
if (CONTINUATION * economics.miss - economics.warm < MINIMUM_SAVINGS)
|
|
151
|
+
return this.stop("refresh would not save money");
|
|
152
|
+
if (active.spent + economics.warm > (this.options.maxHoldCost ?? 2)) return this.stop("hold spend limit");
|
|
153
|
+
let usage;
|
|
154
|
+
try {
|
|
155
|
+
usage = await this.send(lane.model, PROTOCOLS[lane.model.api].cap(lane.payload), active.ctx);
|
|
156
|
+
} catch {
|
|
157
|
+
usage = undefined;
|
|
158
|
+
}
|
|
159
|
+
if (this.active !== active) return;
|
|
160
|
+
if (!usage) return this.stop("refresh failed");
|
|
161
|
+
const spent = cost(lane.model, usage);
|
|
162
|
+
active.spent += spent;
|
|
163
|
+
try {
|
|
164
|
+
this.options.onRefresh?.({ provider: lane.model.provider, model: lane.model.id, usage, cost: spent });
|
|
165
|
+
} catch {
|
|
166
|
+
// Attribution is diagnostic and never changes warming.
|
|
167
|
+
}
|
|
168
|
+
if (usage.cacheRead < REUSE * lane.prompt || usage.cacheWrite > (1 - REUSE) * lane.prompt)
|
|
169
|
+
return this.stop("refresh did not reuse the cache");
|
|
170
|
+
lane.sentAt = now;
|
|
171
|
+
this.schedule();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Pi's refresh point: 90% of the lifetime, keeping at least ten seconds of margin. */
|
|
175
|
+
const refreshDelay = (ttl) => Math.min(ttl * 0.9, ttl - EXPIRY_MARGIN_MS);
|
|
176
|
+
function laneKey(ctx, hold, requester) {
|
|
177
|
+
const provider = hold?.provider ?? ctx?.model?.provider,
|
|
178
|
+
model = hold?.modelId ?? ctx?.model?.id;
|
|
179
|
+
if (!provider || !model) return;
|
|
180
|
+
return JSON.stringify([ctx?.sessionManager?.getSessionId?.() ?? "memory", provider, model, requester ?? ""]);
|
|
181
|
+
}
|
|
182
|
+
const perToken = (value) => (typeof value === "number" ? value / 1_000_000 : undefined);
|
|
183
|
+
function price(lane) {
|
|
184
|
+
const c = lane.model.cost ?? {};
|
|
185
|
+
const read = perToken(c.cacheRead),
|
|
186
|
+
input = perToken(c.input),
|
|
187
|
+
output = perToken(c.output) ?? 0;
|
|
188
|
+
const write = perToken(c.cacheWrite) || input;
|
|
189
|
+
if (!lane.prompt || read === undefined || write === undefined) return;
|
|
190
|
+
return { warm: lane.prompt * read + output, miss: Math.max(0, lane.prompt * (write - read)) };
|
|
191
|
+
}
|
|
192
|
+
function cost(model, usage) {
|
|
193
|
+
const c = model.cost ?? {};
|
|
194
|
+
return (
|
|
195
|
+
usage.input * (perToken(c.input) ?? 0) +
|
|
196
|
+
usage.output * (perToken(c.output) ?? 0) +
|
|
197
|
+
usage.cacheRead * (perToken(c.cacheRead) ?? 0) +
|
|
198
|
+
usage.cacheWrite * (perToken(c.cacheWrite) ?? 0)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
/** Replay through the host's own transport so provider auth and request shaping apply as usual. */
|
|
202
|
+
async function sendThroughHost(model, payload, ctx) {
|
|
203
|
+
const stream = ctx.modelRegistry.streamSimple(
|
|
204
|
+
model,
|
|
205
|
+
{ systemPrompt: "", messages: [{ role: "user", content: "cache keep-alive", timestamp: Date.now() }] },
|
|
206
|
+
{ onPayload: () => payload, maxRetries: 0, maxTokens: 1 },
|
|
207
|
+
);
|
|
208
|
+
const message = await stream.result();
|
|
209
|
+
return message.stopReason === "error" || message.stopReason === "aborted" ? undefined : message.usage;
|
|
210
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-cache diagnostics for /freeflow status. The footer only shows current Freeflow settings, so
|
|
3
|
+
* cache mechanics (keep-alive, context replay, effort baselines) report here instead.
|
|
4
|
+
*/
|
|
5
|
+
export class CacheMonitor {
|
|
6
|
+
entries = new Map();
|
|
7
|
+
set(key, text) {
|
|
8
|
+
if (text) this.entries.set(key, text);
|
|
9
|
+
else this.entries.delete(key);
|
|
10
|
+
}
|
|
11
|
+
lines() {
|
|
12
|
+
return [...this.entries.values()];
|
|
13
|
+
}
|
|
14
|
+
reset() {
|
|
15
|
+
this.entries.clear();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI Responses on GPT-5.6+: the implicit breakpoint sits at the latest eligible message, and a
|
|
4
|
+
* lookup checks it, up to 20 earlier eligible message endings, and the latest explicit breakpoints.
|
|
5
|
+
* A position here is an eligible message ending: a user message, or the last tool output of a group.
|
|
6
|
+
*/
|
|
7
|
+
export const OPENAI_RESPONSES_LIMITS = { maxBreakpoints: 50, lookback: 20, margin: 5 };
|
|
8
|
+
const OUTPUTS = new Set(["function_call_output", "custom_tool_call_output"]);
|
|
9
|
+
const withoutMarkers = (value) =>
|
|
10
|
+
JSON.stringify(value, (key, v) => (key === "prompt_cache_breakpoint" ? undefined : v));
|
|
11
|
+
/** Only routes whose documented cache protocol includes explicit breakpoints are adapted. */
|
|
12
|
+
export const supportsExplicitBreakpoints = (model) => model?.compat?.supportsExplicitPromptCacheMode === true;
|
|
13
|
+
export function responsesLayout(payload) {
|
|
14
|
+
if (!payload || !Array.isArray(payload.input)) return;
|
|
15
|
+
const chain = [],
|
|
16
|
+
handles = [],
|
|
17
|
+
breakpoints = [];
|
|
18
|
+
let hash = createHash("sha256")
|
|
19
|
+
.update(withoutMarkers([payload.instructions ?? null, payload.tools ?? null]))
|
|
20
|
+
.digest("hex");
|
|
21
|
+
let pending = "";
|
|
22
|
+
const input = payload.input;
|
|
23
|
+
input.forEach((item, i) => {
|
|
24
|
+
pending += withoutMarkers(item);
|
|
25
|
+
const user = item?.role === "user";
|
|
26
|
+
const lastOutput = OUTPUTS.has(item?.type) && !OUTPUTS.has(input[i + 1]?.type);
|
|
27
|
+
if (!user && !lastOutput) return;
|
|
28
|
+
hash = createHash("sha256").update(hash).update(pending).digest("hex");
|
|
29
|
+
pending = "";
|
|
30
|
+
const content = Array.isArray(item.content) ? item.content : undefined;
|
|
31
|
+
if (content?.some((block) => block?.prompt_cache_breakpoint)) breakpoints.push(chain.length);
|
|
32
|
+
chain.push(hash);
|
|
33
|
+
// Breakpoints go on message content blocks; tool outputs have no documented place for one.
|
|
34
|
+
handles.push(user && content?.length ? { item: i, block: content.length - 1 } : undefined);
|
|
35
|
+
});
|
|
36
|
+
// The implicit breakpoint: the latest eligible message ending.
|
|
37
|
+
if (chain.length && breakpoints.at(-1) !== chain.length - 1) breakpoints.push(chain.length - 1);
|
|
38
|
+
return { layout: { chain, breakpoints, yieldable: [] }, handles };
|
|
39
|
+
}
|
|
40
|
+
export function applyResponsesAnchor(payload, { handles }, plan) {
|
|
41
|
+
const handle = handles[plan.add];
|
|
42
|
+
if (!handle) return payload;
|
|
43
|
+
const input = [...payload.input];
|
|
44
|
+
const item = { ...input[handle.item], content: [...input[handle.item].content] };
|
|
45
|
+
item.content[handle.block] = { ...item.content[handle.block], prompt_cache_breakpoint: { mode: "explicit" } };
|
|
46
|
+
input[handle.item] = item;
|
|
47
|
+
return { ...payload, input };
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { effortHistoryRoute } from "./openai/adapter.js";
|
|
2
|
+
import { isChatGPTSignIn } from "./routes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether changing effort on this route keeps its prompt cache. Freeflow's effort history keeps it on qualified
|
|
5
|
+
* GPT-6 routes; Pi keeps it natively on Claude models that take per-message effort, where a request-level
|
|
6
|
+
* effort change would otherwise invalidate every cached message.
|
|
7
|
+
*/
|
|
8
|
+
export function keepsCacheAcrossEffort(model, ctx) {
|
|
9
|
+
// Sign in with ChatGPT rejects effort-history items, and a plain effort change rereads the context (live, S-006).
|
|
10
|
+
if (isChatGPTSignIn(model, ctx)) return false;
|
|
11
|
+
if (effortHistoryRoute(model) !== undefined) return true;
|
|
12
|
+
return model?.api === "anthropic-messages" && model.compat?.supportsMidConvoEffort === true;
|
|
13
|
+
}
|
|
@@ -1,4 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { CacheAnchorAdapter } from "./cache/index.js";
|
|
2
|
+
import { CacheKeepAlive } from "./cache/keepalive.js";
|
|
3
|
+
import { registerOpenAIEffortSupport } from "./openai/index.js";
|
|
4
|
+
export function registerProviderSupport(pi, enabled, monitor) {
|
|
5
|
+
registerOpenAIEffortSupport(pi, enabled, monitor);
|
|
6
|
+
const anchor = new CacheAnchorAdapter(enabled);
|
|
7
|
+
const report = () => monitor?.set("keep-alive", keepAlive.status());
|
|
8
|
+
const keepAlive = new CacheKeepAlive({
|
|
9
|
+
enabled,
|
|
10
|
+
onRefresh: (record) => {
|
|
11
|
+
try {
|
|
12
|
+
// Pi's session usage totals are host-owned; keep-alive spend is recorded for Freeflow attribution.
|
|
13
|
+
pi.appendEntry?.("freeflow-cache-keepalive-v1", { ...record, at: Date.now() });
|
|
14
|
+
} catch {}
|
|
15
|
+
report();
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const evaluate = (ctx) => {
|
|
19
|
+
keepAlive.evaluate(ctx);
|
|
20
|
+
report();
|
|
21
|
+
};
|
|
22
|
+
pi.on("session_shutdown", () => {
|
|
23
|
+
anchor.reset();
|
|
24
|
+
keepAlive.reset();
|
|
25
|
+
report();
|
|
26
|
+
});
|
|
27
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
28
|
+
const sent = anchor.adapt(event.payload, ctx);
|
|
29
|
+
try {
|
|
30
|
+
keepAlive.record(sent, ctx);
|
|
31
|
+
evaluate(ctx);
|
|
32
|
+
} catch {
|
|
33
|
+
// Keep-alive bookkeeping never affects the request being sent.
|
|
34
|
+
}
|
|
35
|
+
return sent;
|
|
36
|
+
});
|
|
37
|
+
pi.on("message_end", (event, ctx) => {
|
|
38
|
+
const message = event?.message;
|
|
39
|
+
if (message?.role === "assistant" && message.provider && message.model)
|
|
40
|
+
keepAlive.observe(ctx, message.usage, { provider: message.provider, modelId: message.model });
|
|
41
|
+
});
|
|
42
|
+
pi.on("agent_start", (_event, ctx) => evaluate(ctx));
|
|
43
|
+
pi.on("agent_end", (_event, ctx) => evaluate(ctx));
|
|
44
|
+
return { keepAlive };
|
|
4
45
|
}
|