@hassangameryt/freeflow 0.7.2 → 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.
Files changed (267) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/NOTICE +14 -0
  6. package/README.md +151 -136
  7. package/capabilities/cognitive-routing/SKILL.md +110 -96
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +40 -21
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/cognitive-routing/event-store.js +280 -0
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/cognitive-routing/projection.js +503 -0
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +31 -10
  31. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  32. package/pi-extension/dist/cognitive-routing/schemas.js +209 -0
  33. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  34. package/pi-extension/dist/cognitive-routing/sources.js +291 -0
  35. package/pi-extension/dist/cognitive-routing/state.js +487 -0
  36. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  37. package/pi-extension/dist/cognitive-routing/tools.js +60 -0
  38. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +10 -12
  39. package/pi-extension/dist/compaction/carry.js +191 -0
  40. package/pi-extension/dist/compaction/config.js +22 -0
  41. package/pi-extension/dist/compaction/controller.js +471 -0
  42. package/pi-extension/dist/compaction/harness.js +77 -0
  43. package/pi-extension/dist/compaction/results.js +158 -0
  44. package/pi-extension/dist/compaction/thresholds.js +35 -0
  45. package/pi-extension/dist/compaction/tool.js +57 -0
  46. package/pi-extension/dist/host/branch.js +13 -0
  47. package/pi-extension/dist/host/canonical.js +12 -0
  48. package/pi-extension/dist/host/catalog.js +77 -0
  49. package/pi-extension/dist/host/config.js +374 -0
  50. package/pi-extension/dist/host/projection-tags.js +60 -0
  51. package/pi-extension/dist/host/prompts.js +82 -0
  52. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  53. package/pi-extension/dist/host/request-history.js +298 -0
  54. package/pi-extension/dist/host/runtime-state.js +189 -0
  55. package/pi-extension/dist/host/settings/freeflow-command.js +1570 -0
  56. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  57. package/pi-extension/dist/host/staging.js +28 -0
  58. package/pi-extension/dist/host/status.js +101 -0
  59. package/pi-extension/dist/index.js +441 -170
  60. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  61. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  62. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  63. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  64. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  65. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  66. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  67. package/pi-extension/dist/provider-support/effort.js +13 -0
  68. package/pi-extension/dist/provider-support/index.js +45 -0
  69. package/pi-extension/dist/provider-support/openai/adapter.js +127 -0
  70. package/pi-extension/dist/provider-support/openai/history.js +127 -0
  71. package/pi-extension/dist/provider-support/openai/index.js +15 -0
  72. package/pi-extension/dist/provider-support/openai/session-state.js +105 -0
  73. package/pi-extension/dist/provider-support/routes.js +17 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  78. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  79. package/pi-extension/dist/tool-execution/background.js +238 -0
  80. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  81. package/pi-extension/dist/tool-execution/config.js +22 -0
  82. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  83. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  84. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  85. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  86. package/pi-extension/dist/tool-execution/tools.js +16 -0
  87. package/pi-extension/freeflow/README.md +9 -0
  88. package/plugin.json +1 -1
  89. package/runtime/prompts/cognitive-routing.md +4 -4
  90. package/runtime/prompts/core.md +22 -75
  91. package/runtime/prompts/interaction-contract.md +15 -8
  92. package/runtime/prompts/tool-execution.md +38 -0
  93. package/runtime/prompts/working-method.md +108 -0
  94. package/skills/action-selection/SKILL.md +39 -47
  95. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  96. package/skills/bypass/SKILL.md +2 -2
  97. package/skills/commit-work/SKILL.md +31 -83
  98. package/skills/commit-work/references/git-state-edges.md +1 -1
  99. package/skills/commit-work/references/simple-push.md +2 -0
  100. package/skills/commit-work/references/staging-decisions.md +1 -1
  101. package/skills/decision-gate/SKILL.md +31 -50
  102. package/skills/design-for-depth/SKILL.md +44 -53
  103. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  104. package/skills/design-for-depth/references/implementation-decisions.md +4 -4
  105. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  106. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  107. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  108. package/skills/diagnose-failure/SKILL.md +39 -24
  109. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  110. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  111. package/skills/discuss/SKILL.md +37 -13
  112. package/skills/discuss/references/discussion-continuity.md +7 -27
  113. package/skills/evaluate-skill/SKILL.md +49 -80
  114. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  115. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  116. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  117. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  118. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  119. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  120. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  121. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  122. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  123. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  124. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  125. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  126. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  127. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  128. package/skills/execute-work/SKILL.md +41 -9
  129. package/skills/execute-work/references/code-practices.md +2 -0
  130. package/skills/execute-work/references/execute-work-edges.md +11 -1
  131. package/skills/handoff/SKILL.md +1 -1
  132. package/skills/handoff/references/handoff-templates.md +1 -1
  133. package/skills/review-artifact/SKILL.md +42 -41
  134. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  135. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  136. package/skills/review-work/SKILL.md +38 -32
  137. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  138. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  139. package/skills/review-work/references/security-risk-lens.md +2 -4
  140. package/skills/setup-freeflow/SKILL.md +3 -1
  141. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  142. package/skills/setup-freeflow/references/host-setup.md +3 -3
  143. package/skills/track-work/SKILL.md +113 -161
  144. package/skills/track-work/references/working-record-format.md +189 -350
  145. package/skills/track-work/scripts/lib/document.mjs +62 -6
  146. package/skills/track-work/scripts/lib/format.mjs +16 -1
  147. package/skills/track-work/scripts/lib/store.mjs +9 -8
  148. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  149. package/skills/track-work/scripts/working-record.mjs +34 -12
  150. package/skills/verify-work/SKILL.md +29 -20
  151. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  152. package/skills/verify-work/references/integration-evidence.md +2 -0
  153. package/skills/verify-work/references/performance-evidence.md +3 -1
  154. package/skills/verify-work/references/test-design.md +3 -1
  155. package/skills/workflow/SKILL.md +65 -21
  156. package/skills/workflow/references/domain-skill-composition.md +5 -3
  157. package/skills/write-docs/SKILL.md +83 -0
  158. package/skills/write-docs/references/documentation-shapes.md +119 -0
  159. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  160. package/skills/write-plan/SKILL.md +40 -40
  161. package/skills/write-plan/references/plan-shapes.md +1 -1
  162. package/skills/write-skill/SKILL.md +107 -147
  163. package/skills/write-skill/references/activation-boundaries.md +2 -0
  164. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  165. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  166. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  167. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  168. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  169. package/skills/write-spec/SKILL.md +34 -41
  170. package/skills/write-spec/references/artifact-standards.md +1 -1
  171. package/skills/write-spec/references/decision-records.md +2 -0
  172. package/skills/write-spec/references/spec-shapes.md +1 -1
  173. package/capabilities/context-virtualization/SKILL.md +0 -103
  174. package/capabilities/conversation-history/SKILL.md +0 -53
  175. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  176. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  177. package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -321
  178. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -1585
  179. package/pi-extension/dist/cognitive-routing-v2/schemas.js +0 -107
  180. package/pi-extension/dist/cognitive-routing-v2/state.js +0 -612
  181. package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -91
  182. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  183. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  184. package/pi-extension/dist/context-virtualization/runtime.js +0 -356
  185. package/pi-extension/dist/context-virtualization/state.js +0 -57
  186. package/pi-extension/dist/context-virtualization/types.js +0 -10
  187. package/pi-extension/dist/conversation-history/passages.js +0 -66
  188. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  189. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  190. package/pi-extension/dist/conversation-history/search.js +0 -56
  191. package/pi-extension/dist/conversation-history/types.js +0 -1
  192. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  193. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  194. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  195. package/pi-extension/dist/freeflow-context/tool.js +0 -313
  196. package/pi-extension/dist/freeflow-context/types.js +0 -12
  197. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  198. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  199. package/pi-extension/dist/runtime/runtime-context.js +0 -735
  200. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  201. package/pi-extension/dist/session-sources/events.js +0 -195
  202. package/pi-extension/dist/session-sources/sources.js +0 -233
  203. package/pi-extension/dist/settings/settings-ui.js +0 -1160
  204. package/pi-extension/dist/ui/text-component.js +0 -57
  205. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  206. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -143
  207. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -365
  208. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -256
  209. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -1634
  210. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -109
  211. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -622
  212. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -93
  213. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -222
  214. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  215. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  216. package/pi-extension/src/context-virtualization/runtime.ts +0 -406
  217. package/pi-extension/src/context-virtualization/state.ts +0 -79
  218. package/pi-extension/src/context-virtualization/types.ts +0 -92
  219. package/pi-extension/src/conversation-history/passages.ts +0 -81
  220. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  221. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  222. package/pi-extension/src/conversation-history/search.ts +0 -79
  223. package/pi-extension/src/conversation-history/types.ts +0 -31
  224. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  225. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  226. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  227. package/pi-extension/src/freeflow-context/tool.ts +0 -365
  228. package/pi-extension/src/freeflow-context/types.ts +0 -70
  229. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  230. package/pi-extension/src/index.ts +0 -368
  231. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  232. package/pi-extension/src/runtime/runtime-context.ts +0 -838
  233. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  234. package/pi-extension/src/session-sources/events.ts +0 -208
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -263
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1422
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -484
  241. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  242. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -441
  243. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  244. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -245
  245. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  246. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -236
  247. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -230
  248. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -153
  249. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  250. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -275
  251. package/pi-extension/tests/context-virtualization/extension.test.js +0 -765
  252. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  253. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -216
  254. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  255. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  256. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  257. package/pi-extension/tests/fixtures/routing-native.js +0 -188
  258. package/pi-extension/tests/integration/mode-free-core.test.js +0 -219
  259. package/pi-extension/tests/integration/pi-extension.test.js +0 -968
  260. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -431
  261. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  262. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  263. package/pi-extension/tsconfig.json +0 -16
  264. package/runtime/prompts/context-virtualization.md +0 -3
  265. package/runtime/prompts/conversation-history.md +0 -3
  266. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  267. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
@@ -0,0 +1,467 @@
1
+ import { ROUTING_RECOVERY_HINT } from "./session.js";
2
+ export { ROUTING_RECOVERY_HINT };
3
+ import { sessionStage, stagedFor } from "../host/staging.js";
4
+ import { resolveCognitiveRoutingState } from "./config.js";
5
+ import { presetWarnings } from "./economics.js";
6
+ import { keepsCacheAcrossEffort } from "../provider-support/effort.js";
7
+ import { workersForDelegation, RoutingError, isWorkerProfile, requireCondition as check, samePair } from "./types.js";
8
+ /**
9
+ * The only part of routing that changes the host model and effort: manual holds and releases, session presets,
10
+ * delegation mode, switches staged until the next prompt, and preset advice. Idle switches are recorded and applied
11
+ * as the next prompt starts; switches during a run apply at once, and a recorded profile never names a model the
12
+ * host is not running.
13
+ */
14
+ export class ModelControl {
15
+ session;
16
+ pi;
17
+ constructor(session, pi) {
18
+ this.session = session;
19
+ this.pi = pi;
20
+ }
21
+ applying;
22
+ externalChange = false;
23
+ heldNotice;
24
+ /** A manual hold whose profile model is not what the host runs, e.g. after a change in Pi's model picker. */
25
+ heldMismatch(state = this.session.stateData()) {
26
+ if (!this.session.supported() || state.control !== "manual" || !state.profile || this.applying) return;
27
+ const observed = this.intended();
28
+ let expected;
29
+ try {
30
+ expected = this.session.profilePair(state.profile);
31
+ } catch {
32
+ return;
33
+ }
34
+ return observed && !samePair(observed, expected) ? { profile: state.profile, expected, observed } : undefined;
35
+ }
36
+ /** Set by a user prompt's before_agent_start; consumed by the run it starts. */
37
+ promptRun = false;
38
+ /** A run that no user prompt prepared has started and its first message has not been seen yet. */
39
+ unpreparedRun = false;
40
+ /** The next run is already prepared by routing itself (explicit resume sends the message that starts it). */
41
+ preparedRun() {
42
+ this.promptRun = true;
43
+ }
44
+ /** Pi starts a run: from a user prompt (prepared in beforeRun), a message an extension sent, or a retry. */
45
+ runStarted() {
46
+ this.unpreparedRun = !this.promptRun;
47
+ this.promptRun = false;
48
+ }
49
+ /**
50
+ * Whether this message starts a run an extension's message triggered. Pi skips before_agent_start for those runs,
51
+ * so routing prepares them here, before their first request. A retry's run starts with no message and is left as is.
52
+ */
53
+ startedByMessage(message) {
54
+ if (!this.unpreparedRun) return false;
55
+ this.unpreparedRun = false;
56
+ return message?.role === "custom";
57
+ }
58
+ async beforeRun(ctx, fromPrompt = true) {
59
+ if (fromPrompt) this.promptRun = true;
60
+ this.session.ctx = ctx;
61
+ if (this.session.store && this.session.supported() && !this.session.store.blocked && !this.session.error) {
62
+ if (this.session.stateData().control === "automatic" && isWorkerProfile(this.session.stateData().profile)) {
63
+ const result = await this.control("coordinator", false);
64
+ check(result.status !== "blocked", "reconciliation_required", result.reason);
65
+ }
66
+ // A switch made while idle reaches Pi's model now, as the prompt starts and before it is recorded.
67
+ const target = this.pending();
68
+ try {
69
+ await this.applyPending();
70
+ } catch (error) {
71
+ const current = this.session.observed();
72
+ ctx.ui?.notify?.(
73
+ `Could not switch to ${target?.modelId}/${target?.thinking} (${error instanceof Error ? error.message : String(error)}); this prompt runs on ${current?.modelId}/${current?.thinking}.`,
74
+ "warning",
75
+ );
76
+ }
77
+ }
78
+ const held = this.session.store ? this.heldMismatch() : undefined;
79
+ const notice = held && JSON.stringify(held);
80
+ // The hold is the user's; report the divergence once instead of overriding a deliberate model choice.
81
+ if (notice && notice !== this.heldNotice)
82
+ ctx.ui?.notify?.(
83
+ `Manual hold is ${held.profile} (${held.expected.modelId}/${held.expected.thinking}) but Pi is using ${held.observed.modelId}/${held.observed.thinking}. Run /freeflow profile ${held.profile} to reapply it, or /freeflow profile auto to release the hold.`,
84
+ "warning",
85
+ );
86
+ this.heldNotice = notice;
87
+ }
88
+ async validateProfilePairs(pairs) {
89
+ check(pairs.coordinator, "profile_missing", "Configure the coordinator profile.");
90
+ const helper = pairs.helper !== undefined;
91
+ const executor = pairs.executor !== undefined;
92
+ check(helper || executor, "profile_missing", "Configure an enabled worker profile.");
93
+ const delegation = helper && executor ? "both" : helper ? "helper" : "executor";
94
+ const profiles = Object.fromEntries(
95
+ Object.entries(pairs).map(([profile, pair]) => [
96
+ profile,
97
+ { provider: pair.provider, model: pair.modelId, thinking: pair.thinking },
98
+ ]),
99
+ );
100
+ const result = await resolveCognitiveRoutingState(
101
+ { cognitiveRouting: { enabled: true, delegation, profiles } },
102
+ {},
103
+ this.session.ctx,
104
+ );
105
+ check(result.effective, result.blockingReason.code, result.blockingReason.message);
106
+ }
107
+ model(profile) {
108
+ const pair = this.session.profilePair(profile);
109
+ const model = this.session.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
110
+ check(model, "profile_unavailable");
111
+ return model;
112
+ }
113
+ /** Advisory preset checks for the effective Coordinator and enabled workers. */
114
+ presetWarnings(state = this.session.stateData()) {
115
+ if (!this.session.supported()) return [];
116
+ const pairs = {};
117
+ for (const profile of ["coordinator", ...this.session.enabledWorkers(state)])
118
+ try {
119
+ pairs[profile] = this.session.profilePair(profile);
120
+ } catch {}
121
+ return presetWarnings(
122
+ pairs,
123
+ this.session.enabledWorkers(state),
124
+ (provider, modelId) => this.session.ctx?.modelRegistry?.find?.(provider, modelId),
125
+ (model) => keepsCacheAcrossEffort(model, this.session.ctx),
126
+ // Pi's only retention control; unset means the short tier.
127
+ process.env.PI_CACHE_RETENTION === "long" ? "long" : "short",
128
+ );
129
+ }
130
+ presetNotice;
131
+ announcePresets(ctx = this.session.ctx) {
132
+ const warnings = this.presetWarnings();
133
+ const notice = warnings.length ? JSON.stringify(warnings) : undefined;
134
+ if (notice && notice !== this.presetNotice)
135
+ ctx?.ui?.notify?.(`Cognitive Routing preset: ${warnings.join(" ")}`, "warning");
136
+ this.presetNotice = notice;
137
+ }
138
+ /**
139
+ * The user picked a model or effort in Pi itself. Under automatic control that ends routing's ownership of the
140
+ * model: returns true when it did, so the caller turns routing off for the session where the user can see it.
141
+ */
142
+ async nativeChange(ctx) {
143
+ this.session.ctx = ctx;
144
+ if (!this.session.supported() || !this.session.store) return false;
145
+ if (this.applying) {
146
+ if (ctx.model?.provider !== this.applying.provider || ctx.model?.id !== this.applying.modelId) {
147
+ this.externalChange = true;
148
+ this.session.revision++;
149
+ }
150
+ return false; // Never await a queue already held by our setter.
151
+ }
152
+ // The user's own pick in Pi wins over a switch still waiting for the next prompt.
153
+ this.setPending(undefined);
154
+ const state = this.session.stateData();
155
+ if (
156
+ state.control !== "automatic" ||
157
+ !state.profile ||
158
+ samePair(this.session.observed(), this.session.profilePair(state.profile))
159
+ )
160
+ return false;
161
+ try {
162
+ this.session.revision++;
163
+ this.session.manualHold = undefined;
164
+ this.session.automaticControl = false;
165
+ this.session.append({ type: "control", control: "inactive", reason: "External native model/effort change" });
166
+ const now = this.session.observed();
167
+ ctx.ui?.notify?.(
168
+ `Cognitive Routing is off for this session: the model or effort was changed outside routing${now ? ` (now ${now.modelId}/${now.thinking})` : ""}. Turn it back on with /freeflow profile auto, Ctrl+Shift+A, or /freeflow settings.`,
169
+ "warning",
170
+ );
171
+ return true;
172
+ } catch (error) {
173
+ this.session.mark(error);
174
+ return false;
175
+ }
176
+ }
177
+ async applyPair(target) {
178
+ const subject = this.session.subject(),
179
+ token = this.session.token,
180
+ prior = this.session.observed();
181
+ if (samePair(prior, target)) return;
182
+ const model = this.session.ctx.modelRegistry.find(target.provider, target.modelId);
183
+ check(model, "profile_unavailable");
184
+ const auth = await this.session.ctx.modelRegistry.getApiKeyAndHeaders(model);
185
+ this.session.guard(subject);
186
+ check(auth?.ok, "profile_unauthenticated");
187
+ this.applying = target;
188
+ let ownedPair = target;
189
+ this.externalChange = false;
190
+ try {
191
+ check(await this.pi.setModel(model), "model_rejected");
192
+ this.session.guard(subject);
193
+ this.pi.setThinkingLevel(target.thinking);
194
+ check(!this.externalChange && samePair(this.session.observed(), target), "configuration_mismatch");
195
+ } catch (error) {
196
+ if (this.session.current(subject) && prior && !this.externalChange) {
197
+ const old = this.session.ctx.modelRegistry.find(prior.provider, prior.modelId);
198
+ try {
199
+ this.applying = prior;
200
+ ownedPair = prior;
201
+ check(old && (await this.pi.setModel(old)), "rollback_failed");
202
+ this.session.guard(subject);
203
+ this.pi.setThinkingLevel(prior.thinking);
204
+ check(samePair(this.session.observed(), prior), "rollback_failed");
205
+ } catch {
206
+ if (this.session.current(subject))
207
+ this.session.error = "configuration_unknown: rollback could not be observed";
208
+ }
209
+ }
210
+ throw error;
211
+ } finally {
212
+ if (token === this.session.token && this.applying === ownedPair) this.applying = undefined;
213
+ }
214
+ }
215
+ /** A profile switch made while idle, kept with the session's stage so a reload keeps it. */
216
+ pending() {
217
+ return stagedFor(this.session.ctx?.sessionManager)?.pair;
218
+ }
219
+ setPending(pair) {
220
+ const stage = pair ? sessionStage(this.session.ctx?.sessionManager) : stagedFor(this.session.ctx?.sessionManager);
221
+ if (!stage) return;
222
+ if (pair) stage.pair = pair;
223
+ else delete stage.pair;
224
+ }
225
+ /** The pair routing intends the host to run: a pending switch, or what the host runs now. */
226
+ intended() {
227
+ return this.pending() ?? this.session.observed();
228
+ }
229
+ async checkPair(target) {
230
+ const model = this.session.ctx.modelRegistry.find(target.provider, target.modelId);
231
+ check(model, "profile_unavailable");
232
+ check((await this.session.ctx.modelRegistry.getApiKeyAndHeaders(model))?.ok, "profile_unauthenticated");
233
+ }
234
+ /**
235
+ * Apply a switch made while idle, so the prompt runs on the profile the user chose. If the host cannot run it,
236
+ * the switch and the control changes staged with it are dropped, so recorded control still names what runs.
237
+ */
238
+ async applyPending() {
239
+ const target = this.pending();
240
+ if (!target) return;
241
+ try {
242
+ await this.applyPair(target);
243
+ } catch (error) {
244
+ const stage = stagedFor(this.session.ctx?.sessionManager);
245
+ if (stage) stage.events = [];
246
+ throw error;
247
+ } finally {
248
+ this.setPending(undefined);
249
+ }
250
+ }
251
+ /**
252
+ * Switch the host model, then record what the switch means. A switch that fails records nothing, and a
253
+ * record that cannot be written restores the prior model, so recorded control never names a profile
254
+ * whose model the host is not running.
255
+ */
256
+ async transition(target, record, subject) {
257
+ if (this.session.ctx?.isIdle?.() !== false) {
258
+ // An idle switch reaches Pi's model at the next prompt; checking the target now keeps errors immediate.
259
+ if (!samePair(this.intended(), target)) await this.checkPair(target);
260
+ this.session.guard(subject);
261
+ record();
262
+ this.setPending(samePair(this.session.observed(), target) ? undefined : target);
263
+ return;
264
+ }
265
+ this.setPending(undefined);
266
+ const prior = this.session.observed();
267
+ await this.applyPair(target);
268
+ try {
269
+ this.session.guard(subject);
270
+ record();
271
+ } catch (error) {
272
+ if (subject.token === this.session.token && prior && !samePair(this.session.observed(), prior))
273
+ await this.applyPair(prior).catch(() => {});
274
+ throw error;
275
+ }
276
+ }
277
+ /**
278
+ * Control requests run one at a time, each against the state left by the previous one. The host model
279
+ * is switched before the new control is recorded, so a superseded or failed request never leaves a
280
+ * recorded profile running on another profile's model.
281
+ */
282
+ async control(request, manual) {
283
+ return this.session.enqueue(async () => {
284
+ this.session.revision++;
285
+ const subject = this.session.subject();
286
+ try {
287
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
288
+ await this.session.store.reconcile();
289
+ this.session.guard(subject);
290
+ const previous = this.session.stateData();
291
+ const profile = typeof request === "function" ? request(previous) : request;
292
+ if (manual && isWorkerProfile(profile))
293
+ check(
294
+ this.session.enabledWorkers().includes(profile),
295
+ "worker_disabled",
296
+ `${profile} is not enabled by delegation mode.`,
297
+ );
298
+ this.session.error = undefined;
299
+ this.session.suppressed = false;
300
+ if (
301
+ previous.control === (manual ? "manual" : "automatic") &&
302
+ previous.profile === profile &&
303
+ samePair(this.intended(), this.session.profilePair(profile))
304
+ ) {
305
+ this.session.manualHold = manual ? profile : undefined;
306
+ this.session.automaticControl = !manual;
307
+ return { status: manual ? "active" : "automatic" };
308
+ }
309
+ await this.transition(
310
+ this.session.profilePair(profile),
311
+ () =>
312
+ this.session.append({
313
+ type: "control",
314
+ control: manual ? "manual" : "automatic",
315
+ profile,
316
+ reason: manual ? "Explicit user manual hold" : "Explicit user automatic release/reconciliation",
317
+ }),
318
+ subject,
319
+ );
320
+ this.session.manualHold = manual ? profile : undefined;
321
+ this.session.automaticControl = !manual;
322
+ return { status: manual ? "active" : "automatic" };
323
+ } catch (error) {
324
+ if (this.session.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
325
+ this.session.mark(error);
326
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
327
+ }
328
+ });
329
+ }
330
+ /** Advance one manual-hold step from the state left by any earlier queued request. */
331
+ cycleManualProfile() {
332
+ return this.control((state) => {
333
+ const profiles = ["coordinator", ...workersForDelegation(this.session.delegation(state))];
334
+ return profiles[(profiles.indexOf(state.profile ?? "coordinator") + 1) % profiles.length];
335
+ }, true);
336
+ }
337
+ setManualProfile(profile, _mechanism) {
338
+ return this.control(profile, true);
339
+ }
340
+ sessionProfileOverrides() {
341
+ return Object.fromEntries(this.session.stateData().profileOverrides);
342
+ }
343
+ sessionDelegationOverride() {
344
+ return this.session.stateData().delegationOverride;
345
+ }
346
+ async setSessionDelegationOverride(override, mechanism = "Session delegation override") {
347
+ return this.session.enqueue(async () => {
348
+ this.session.revision++;
349
+ const subject = this.session.subject();
350
+ try {
351
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
352
+ check(
353
+ this.session.ctx?.isIdle?.() !== false,
354
+ "host_busy",
355
+ "Wait for Pi to become idle before changing delegation mode.",
356
+ );
357
+ await this.session.store.reconcile();
358
+ this.session.guard(subject);
359
+ const state = this.session.stateData();
360
+ if ((state.delegationOverride ?? null) === override) return { status: "unchanged" };
361
+ const delegation = override ?? this.session.capability.delegation;
362
+ await this.validateProfilePairs(this.session.profilePairs(this.session.requiredProfiles(state, delegation)));
363
+ this.session.guard(subject);
364
+ this.session.append({ type: "delegation-override", delegation: override, reason: mechanism });
365
+ this.announcePresets();
366
+ return { status: "stored" };
367
+ } catch (error) {
368
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
369
+ }
370
+ });
371
+ }
372
+ async setSessionProfileOverride(profile, override, mechanism = "Session profile override") {
373
+ return this.session.enqueue(async () => {
374
+ this.session.revision++;
375
+ const subject = this.session.subject();
376
+ try {
377
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
378
+ check(
379
+ this.session.ctx?.isIdle?.() !== false,
380
+ "host_busy",
381
+ "Wait for Pi to become idle before changing session presets.",
382
+ );
383
+ await this.session.store.reconcile();
384
+ this.session.guard(subject);
385
+ const state = this.session.stateData();
386
+ const previous = state.profileOverrides.get(profile);
387
+ if ((override === null && !previous) || (override !== null && samePair(previous, override)))
388
+ return { status: "unchanged" };
389
+ const target = override ?? this.session.configuredProfilePair(profile);
390
+ const profiles = new Set(this.session.requiredProfiles(state));
391
+ profiles.add(profile);
392
+ const pairs = Object.fromEntries(
393
+ [...profiles].map((name) => [name, name === profile ? target : this.session.profilePair(name)]),
394
+ );
395
+ await this.validateProfilePairs(pairs);
396
+ const record = () =>
397
+ this.session.append({
398
+ type: "profile-overrides",
399
+ overrides: { [profile]: override },
400
+ reason: mechanism,
401
+ });
402
+ if (state.control !== "inactive" && state.profile === profile) {
403
+ await this.transition(target, record, subject);
404
+ this.session.error = undefined;
405
+ this.announcePresets();
406
+ return { status: "active" };
407
+ }
408
+ this.session.guard(subject);
409
+ record();
410
+ this.session.error = undefined;
411
+ this.announcePresets();
412
+ return { status: "stored" };
413
+ } catch (error) {
414
+ if (this.session.current(subject)) this.session.mark(error);
415
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
416
+ }
417
+ });
418
+ }
419
+ async resetSessionProfileOverrides(mechanism = "Reset session profile overrides") {
420
+ return this.session.enqueue(async () => {
421
+ this.session.revision++;
422
+ const subject = this.session.subject();
423
+ try {
424
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
425
+ check(
426
+ this.session.ctx?.isIdle?.() !== false,
427
+ "host_busy",
428
+ "Wait for Pi to become idle before resetting session presets.",
429
+ );
430
+ await this.session.store.reconcile();
431
+ this.session.guard(subject);
432
+ const state = this.session.stateData();
433
+ if (state.profileOverrides.size === 0) return { status: "unchanged" };
434
+ const previous = Object.fromEntries(state.profileOverrides);
435
+ const required = this.session.requiredProfiles(state);
436
+ const targetPairs = Object.fromEntries(
437
+ required.map((profile) => [profile, this.session.configuredProfilePair(profile)]),
438
+ );
439
+ await this.validateProfilePairs(targetPairs);
440
+ const record = () =>
441
+ this.session.append({
442
+ type: "profile-overrides",
443
+ overrides: { coordinator: null, helper: null, executor: null },
444
+ reason: mechanism,
445
+ });
446
+ const active = state.control !== "inactive" ? state.profile : undefined;
447
+ if (active && previous[active]) {
448
+ await this.transition(targetPairs[active], record, subject);
449
+ this.session.error = undefined;
450
+ this.announcePresets();
451
+ return { status: "active" };
452
+ }
453
+ this.session.guard(subject);
454
+ record();
455
+ this.session.error = undefined;
456
+ this.announcePresets();
457
+ return { status: "stored" };
458
+ } catch (error) {
459
+ if (this.session.current(subject)) this.session.mark(error);
460
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
461
+ }
462
+ });
463
+ }
464
+ setAutomaticControl(_mechanism) {
465
+ return this.control("coordinator", false);
466
+ }
467
+ }