@hassangameryt/freeflow 0.7.2 → 0.7.3

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 (79) 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/README.md +147 -127
  6. package/capabilities/cognitive-routing/SKILL.md +162 -93
  7. package/gemini-extension.json +1 -1
  8. package/package.json +1 -1
  9. package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
  10. package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
  11. package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
  12. package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
  13. package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
  14. package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
  15. package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
  16. package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
  17. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  18. package/pi-extension/dist/freeflow-context/tool.js +7 -6
  19. package/pi-extension/dist/index.js +36 -15
  20. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  21. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  22. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  23. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  24. package/pi-extension/dist/provider-support/index.js +4 -0
  25. package/pi-extension/dist/runtime/request-history.js +216 -0
  26. package/pi-extension/dist/runtime/runtime-context.js +29 -10
  27. package/pi-extension/dist/session-sources/events.js +22 -0
  28. package/pi-extension/dist/session-sources/provenance.js +32 -0
  29. package/pi-extension/dist/session-sources/sources.js +51 -7
  30. package/pi-extension/dist/settings/settings-ui.js +247 -47
  31. package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
  32. package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
  33. package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
  34. package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
  35. package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
  36. package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
  37. package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
  38. package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
  39. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  40. package/pi-extension/src/freeflow-context/tool.ts +7 -6
  41. package/pi-extension/src/index.ts +36 -14
  42. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  43. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  44. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  45. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  46. package/pi-extension/src/provider-support/index.ts +5 -0
  47. package/pi-extension/src/runtime/request-history.ts +243 -0
  48. package/pi-extension/src/runtime/runtime-context.ts +32 -10
  49. package/pi-extension/src/session-sources/events.ts +22 -0
  50. package/pi-extension/src/session-sources/provenance.ts +39 -0
  51. package/pi-extension/src/session-sources/sources.ts +77 -6
  52. package/pi-extension/src/settings/settings-ui.ts +276 -46
  53. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  54. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  55. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
  57. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
  59. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
  60. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
  61. package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
  62. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
  63. package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
  64. package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
  65. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  66. package/pi-extension/tests/fixtures/routing-native.js +4 -1
  67. package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
  68. package/pi-extension/tests/integration/pi-extension.test.js +508 -29
  69. package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
  70. package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
  71. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  72. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  73. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  74. package/pi-extension/tests/provider-support/native.test.js +183 -0
  75. package/plugin.json +1 -1
  76. package/runtime/prompts/cognitive-routing.md +4 -4
  77. package/skills/design-for-depth/SKILL.md +1 -1
  78. package/skills/design-for-depth/references/implementation-decisions.md +3 -3
  79. package/skills/discuss/SKILL.md +2 -2
@@ -12,6 +12,7 @@ import {
12
12
  } from "../runtime/runtime-context.js";
13
13
  import { PiSettingsComponent } from "./settings-tui.js";
14
14
  import { isPiFlowHost } from "../runtime/runtime-identity.js";
15
+ import { isWorkerProfile, workersForDelegation } from "../cognitive-routing-v2/types.js";
15
16
  const DEFAULT_FREEFLOW_ENABLED = true;
16
17
  const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
17
18
  const DEFAULT_CONVERSATION_HISTORY_ENABLED = false;
@@ -191,6 +192,68 @@ function createScopedBooleanItem(options) {
191
192
  item.displaySuffix = coreDisplaySuffix(item);
192
193
  return item;
193
194
  }
195
+ function createScopedDelegationItem(options) {
196
+ const path = ["cognitiveRouting", "delegation"];
197
+ const repositoryValue = getPath(options.rawConfig, path);
198
+ const inheritedValue = ["executor", "helper", "both"].includes(repositoryValue) ? repositoryValue : "executor";
199
+ const inheritedSource = repositoryValue === undefined ? "builtin" : "repository";
200
+ const localValue = getPath(options.localConfig, path);
201
+ const descriptions = {
202
+ executor: "Coordinator delegates assignments to Executor.",
203
+ helper: "Coordinator delegates supporting assignments to Helper and keeps production implementation.",
204
+ both: "Coordinator chooses Helper or Executor for each assignment.",
205
+ };
206
+ const item =
207
+ options.scope === "local"
208
+ ? {
209
+ id: "freeflow.cognitiveRouting.delegation",
210
+ label: "Delegation method",
211
+ description:
212
+ "Choose enabled workers for automatic routing. Inherit uses the repository value; this setting does not rank model presets.",
213
+ path,
214
+ kind: "enum",
215
+ value: ["executor", "helper", "both"].includes(localValue) ? localValue : LOCAL_INHERIT,
216
+ values: [LOCAL_INHERIT, "executor", "helper", "both"],
217
+ valueLabels: {
218
+ inherit: "Inherit repository",
219
+ executor: "Executor only",
220
+ helper: "Helper only",
221
+ both: "Helper and Executor",
222
+ },
223
+ valueDescriptions: {
224
+ inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
225
+ ...descriptions,
226
+ },
227
+ configScope: "local",
228
+ configValues: { inherit: undefined, executor: "executor", helper: "helper", both: "both" },
229
+ effectiveValue: options.effectiveValue,
230
+ effectiveSource: options.effectiveSource,
231
+ inheritedValue,
232
+ inheritedSource,
233
+ }
234
+ : {
235
+ id: "freeflow.cognitiveRouting.delegation",
236
+ label: "Delegation method",
237
+ description: "Choose enabled workers for automatic routing. This edits shared .freeflow/config.json.",
238
+ path,
239
+ kind: "enum",
240
+ value: inheritedValue,
241
+ values: ["executor", "helper", "both"],
242
+ valueLabels: {
243
+ executor: "Executor only",
244
+ helper: "Helper only",
245
+ both: "Helper and Executor",
246
+ },
247
+ valueDescriptions: descriptions,
248
+ defaultValue: "executor",
249
+ configScope: "repository",
250
+ effectiveValue: options.effectiveValue,
251
+ effectiveSource: options.effectiveSource,
252
+ localOverrideValue: ["executor", "helper", "both"].includes(localValue) ? localValue : undefined,
253
+ };
254
+ item.displaySuffix = coreDisplaySuffix(item);
255
+ return item;
256
+ }
194
257
  function resolveSettingsCoreView(rawConfig, layers) {
195
258
  const localConfig = layers?.local.valid && isRecord(layers.local.parsed) ? layers.local.parsed : {};
196
259
  const fallbackCore = {
@@ -247,7 +310,7 @@ function createSessionBooleanItem(options) {
247
310
  item.displaySuffix = coreDisplaySuffix(item);
248
311
  return item;
249
312
  }
250
- function sessionFreeflowItems(state, cognitiveRoutingController) {
313
+ function sessionFreeflowItems(state, cognitiveRoutingController, ctx) {
251
314
  const sessionOverrides = state.sessionOverrides;
252
315
  const configured = state.configuredCoreConfig;
253
316
  const configuredSources = state.configuredSources;
@@ -289,47 +352,76 @@ function sessionFreeflowItems(state, cognitiveRoutingController) {
289
352
  contextVirtualizationItem.inactive = freeflowInactive;
290
353
  conversationHistoryItem.inactive = freeflowInactive;
291
354
  const cognitiveRoutingState = cognitiveRoutingController?.state();
292
- const cognitiveRoutingProfile =
293
- cognitiveRoutingState?.controlMode === "manual-executor"
294
- ? "executor"
295
- : cognitiveRoutingState?.controlMode === "manual-coordinator"
296
- ? "coordinator"
297
- : "auto";
355
+ const delegation = state.cognitiveRouting?.delegation ?? "executor";
356
+ const heldProfile = cognitiveRoutingState?.controlMode.startsWith("manual-")
357
+ ? cognitiveRoutingState.activeProfile
358
+ : undefined;
359
+ const availableProfiles = ["coordinator", ...workersForDelegation(delegation)];
360
+ if (heldProfile === "coordinator" || isWorkerProfile(heldProfile)) availableProfiles.push(heldProfile);
361
+ const manualProfiles = [...new Set(availableProfiles)];
362
+ const cognitiveRoutingProfile = heldProfile && manualProfiles.includes(heldProfile) ? heldProfile : "auto";
298
363
  const cognitiveRoutingItem = state.cognitiveRouting
299
364
  ? {
300
365
  id: "freeflow.cognitiveRouting.profile",
301
366
  label: "Cognitive Routing",
302
- description: "Hold a profile manually or release the hold for automatic model control.",
367
+ description: "Hold an enabled profile manually or release the hold for automatic model control.",
303
368
  kind: "enum",
304
369
  value: cognitiveRoutingProfile,
305
- values: ["auto", "executor", "coordinator"],
306
- valueLabels: {
307
- auto: "automatic",
308
- executor: "manual · executor",
309
- coordinator: "manual · coordinator",
310
- },
311
- valueDescriptions: {
312
- auto: "Release the manual hold and return to Coordinator reconciliation.",
313
- executor: "Hold the executor profile until /freeflow profile auto.",
314
- coordinator: "Hold the coordinator profile until /freeflow profile auto.",
315
- },
370
+ values: ["auto", ...manualProfiles],
371
+ valueLabels: Object.fromEntries([
372
+ ["auto", "automatic"],
373
+ ...manualProfiles.map((profile) => [profile, `manual · ${profile}`]),
374
+ ]),
375
+ valueDescriptions: Object.fromEntries([
376
+ ["auto", "Release the manual hold and return to Coordinator reconciliation."],
377
+ ...manualProfiles.map((profile) => [profile, `Hold the ${profile} profile until /freeflow profile auto.`]),
378
+ ]),
316
379
  inactive: freeflowInactive || !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
317
380
  runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
318
381
  displaySuffix: cognitiveRoutingState?.effective ? cognitiveRoutingProfile : "unavailable",
319
382
  }
320
383
  : undefined;
384
+ const sessionProfiles = cognitiveRoutingController?.sessionProfileOverrides();
385
+ const profileItems =
386
+ state.cognitiveRouting && cognitiveRoutingController
387
+ ? manualProfiles.map((name) =>
388
+ cognitiveRoutingProfileItem({
389
+ name,
390
+ scope: "session",
391
+ rawConfig: {},
392
+ localConfig: {},
393
+ capabilityState: state.cognitiveRouting,
394
+ ctx,
395
+ sessionProfiles,
396
+ }),
397
+ )
398
+ : [];
399
+ const cognitiveRoutingPresets = profileItems.length
400
+ ? {
401
+ id: "freeflow.cognitiveRouting.presets",
402
+ label: "Cognitive Routing presets",
403
+ description: "Temporarily choose complete model/effort pairs for profiles enabled in this Pi session.",
404
+ kind: "group",
405
+ value: profileItems.some((item) => item.value !== LOCAL_INHERIT && item.value !== undefined),
406
+ displaySuffix: `${profileItems.filter((item) => item.value !== LOCAL_INHERIT && item.value !== undefined).length}/${profileItems.length} session overrides`,
407
+ inactive: freeflowInactive || !state.cognitiveRouting?.effective || cognitiveRoutingController === undefined,
408
+ children: profileItems,
409
+ }
410
+ : undefined;
321
411
  return [
322
412
  freeflowItem,
323
413
  ...(cognitiveRoutingItem ? [cognitiveRoutingItem] : []),
414
+ ...(cognitiveRoutingPresets ? [cognitiveRoutingPresets] : []),
324
415
  {
325
416
  id: "freeflow.session.reset",
326
417
  label: "Reset session overrides",
327
- description: "Clear Freeflow, Context Virtualization, and Conversation History overrides for this Pi session.",
418
+ description:
419
+ "Clear Freeflow, Context Virtualization, Conversation History, and routing profile overrides for this Pi session.",
328
420
  kind: "enum",
329
421
  value: "available",
330
422
  values: ["reset"],
331
423
  valueLabels: { reset: "Reset all session overrides" },
332
- valueDescriptions: { reset: "Return every session setting to its configured value." },
424
+ valueDescriptions: { reset: "Return every session setting and routing preset to its configured value." },
333
425
  format: () => "available",
334
426
  transient: true,
335
427
  },
@@ -412,12 +504,14 @@ function cognitiveRoutingProfileDisplay(value) {
412
504
  return isCognitiveRoutingProfile(value) ? formatCoreValue(value) : "not configured";
413
505
  }
414
506
  function cognitiveRoutingSettingsSource(source) {
507
+ if (source === "session") return "session";
415
508
  if (source === "repository") return "repository";
416
509
  if (source === "personal" || source === "local") return "local";
417
510
  return "builtin";
418
511
  }
419
512
  function cognitiveRoutingProfileDisplaySuffix(source, scope) {
420
513
  if (!source) return undefined;
514
+ if (source === "session") return "(session override)";
421
515
  if (source === "personal" && scope === "repository") return "(effective personal)";
422
516
  return `(${source})`;
423
517
  }
@@ -436,7 +530,16 @@ function cognitiveRoutingConfirmStep(summary) {
436
530
  selectedKey: "__save__",
437
531
  };
438
532
  }
439
- function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit) {
533
+ function createCognitiveRoutingProfileWizard(
534
+ currentValue,
535
+ models,
536
+ allowInherit,
537
+ inheritChoice = {
538
+ label: "Inherit repository preset",
539
+ description: "Remove the personal override without changing the repository preset.",
540
+ summary: "inherit repository preset",
541
+ },
542
+ ) {
440
543
  const currentProfile = isCognitiveRoutingProfile(currentValue) ? currentValue : undefined;
441
544
  const currentModelKey = currentProfile
442
545
  ? cognitiveRoutingModelKey(currentProfile.provider, currentProfile.model)
@@ -447,8 +550,8 @@ function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit)
447
550
  {
448
551
  key: LOCAL_INHERIT,
449
552
  value: LOCAL_INHERIT,
450
- label: "Inherit repository preset",
451
- description: "Remove the personal override without changing the repository preset.",
553
+ label: inheritChoice.label,
554
+ description: inheritChoice.description,
452
555
  },
453
556
  ]
454
557
  : []),
@@ -482,7 +585,7 @@ function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit)
482
585
  if (selectedValues.length === 1) {
483
586
  const selected = selectedValues[0];
484
587
  if (selected === LOCAL_INHERIT) {
485
- return cognitiveRoutingConfirmStep("inherit repository preset");
588
+ return cognitiveRoutingConfirmStep(inheritChoice.summary);
486
589
  }
487
590
  const model = selected;
488
591
  if (!model?.key || model.thinkingLevels.length === 0) return undefined;
@@ -521,31 +624,59 @@ function cognitiveRoutingProfileItem(options) {
521
624
  const path = ["cognitiveRouting", "profiles", options.name];
522
625
  const repositoryProfile = getPath(options.rawConfig, path);
523
626
  const localProfile = getPath(options.localConfig, path);
524
- const effectiveProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
627
+ const configuredProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
628
+ const sessionPair = options.sessionProfiles?.[options.name];
629
+ const sessionProfile = sessionPair
630
+ ? { provider: sessionPair.provider, model: sessionPair.modelId, thinking: sessionPair.thinking }
631
+ : undefined;
632
+ const effectiveProfile = sessionProfile ?? configuredProfile;
525
633
  const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
526
- const value = options.scope === "local" ? localValue : repositoryProfile;
527
- const source =
634
+ const value =
635
+ options.scope === "session"
636
+ ? (sessionProfile ?? LOCAL_INHERIT)
637
+ : options.scope === "local"
638
+ ? localValue
639
+ : repositoryProfile;
640
+ const configuredSource =
528
641
  options.capabilityState?.profileSources?.[options.name] ??
529
642
  (isCognitiveRoutingProfile(repositoryProfile) ? "repository" : undefined);
643
+ const source = options.scope === "session" ? (sessionProfile ? "session" : configuredSource) : configuredSource;
530
644
  const models = cognitiveRoutingModelOptions(options.ctx);
531
- const inactive = options.scope === "local" && models.length === 0 && localValue !== LOCAL_INHERIT;
645
+ const inactive = options.scope !== "repository" && models.length === 0 && value !== LOCAL_INHERIT;
646
+ const inheritChoice =
647
+ options.scope === "session"
648
+ ? {
649
+ label: "Inherit configured preset",
650
+ description: "Remove the session override without changing local or repository configuration.",
651
+ summary: "inherit configured preset",
652
+ }
653
+ : undefined;
532
654
  return {
533
655
  id: `freeflow.cognitiveRouting.${options.name}`,
534
656
  label: `${options.name.charAt(0).toUpperCase()}${options.name.slice(1)} preset`,
535
657
  description:
536
- "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
658
+ options.scope === "session"
659
+ ? "Temporarily choose the model and effort for this profile in the current Pi session only."
660
+ : "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
537
661
  path,
538
662
  kind: "string",
539
663
  value,
540
- format: cognitiveRoutingProfileDisplay,
664
+ format: (current) => {
665
+ const inherits = current === LOCAL_INHERIT || (options.scope === "session" && current === undefined);
666
+ return inherits
667
+ ? options.scope === "session"
668
+ ? "inherit configured preset"
669
+ : "inherit repository preset"
670
+ : cognitiveRoutingProfileDisplay(current);
671
+ },
541
672
  configScope: options.scope,
542
673
  effectiveValue: effectiveProfile,
543
674
  effectiveSource: cognitiveRoutingSettingsSource(source),
544
- inheritedValue: repositoryProfile,
545
- inheritedSource: "repository",
675
+ inheritedValue: options.scope === "session" ? configuredProfile : repositoryProfile,
676
+ inheritedSource: cognitiveRoutingSettingsSource(configuredSource ?? "builtin"),
546
677
  inactive,
547
678
  displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
548
- wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope === "local"),
679
+ wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope !== "repository", inheritChoice),
549
680
  };
550
681
  }
551
682
  function isCognitiveRoutingRuntimeAvailable(pi) {
@@ -557,7 +688,7 @@ function isCognitiveRoutingRuntimeAvailable(pi) {
557
688
  );
558
689
  }
559
690
  function freeflowItems(rawConfig, options = {}) {
560
- const scope = options.scope ?? "repository";
691
+ const scope = options.scope === "local" ? "local" : "repository";
561
692
  const layers = options.layers;
562
693
  const { localConfig, core, sources } = resolveSettingsCoreView(rawConfig, layers);
563
694
  const freeflowItem = createScopedBooleanItem({
@@ -618,6 +749,15 @@ function freeflowItems(rawConfig, options = {}) {
618
749
  });
619
750
  cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
620
751
  cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
752
+ const cognitiveRoutingDelegationItem = createScopedDelegationItem({
753
+ scope,
754
+ rawConfig,
755
+ localConfig,
756
+ effectiveValue: cognitiveRoutingState?.delegation ?? "executor",
757
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.delegationSource),
758
+ });
759
+ cognitiveRoutingDelegationItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
760
+ cognitiveRoutingDelegationItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
621
761
  const cognitiveRoutingProjectionItem = createScopedBooleanItem({
622
762
  scope,
623
763
  rawConfig,
@@ -625,7 +765,7 @@ function freeflowItems(rawConfig, options = {}) {
625
765
  id: "freeflow.cognitiveRouting.projection",
626
766
  label: "Context projection",
627
767
  description:
628
- "Select Executor evidence for Coordinator; disable projection to keep ordinary Pi context in both profiles.",
768
+ "Select worker evidence for Coordinator; disable projection to keep ordinary Pi context in all enabled profiles.",
629
769
  path: ["cognitiveRouting", "projection"],
630
770
  effectiveValue: cognitiveRoutingState?.projection ?? false,
631
771
  effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.projectionSource),
@@ -633,7 +773,7 @@ function freeflowItems(rawConfig, options = {}) {
633
773
  });
634
774
  cognitiveRoutingProjectionItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
635
775
  cognitiveRoutingProjectionItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
636
- const cognitiveRoutingProfiles = ["executor", "coordinator"].map((name) =>
776
+ const cognitiveRoutingProfiles = ["coordinator", "helper", "executor"].map((name) =>
637
777
  cognitiveRoutingProfileItem({
638
778
  name: name,
639
779
  scope,
@@ -661,12 +801,17 @@ function freeflowItems(rawConfig, options = {}) {
661
801
  label: "Cognitive Routing",
662
802
  description: cognitiveRoutingRuntimeDisabled
663
803
  ? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
664
- : "Configure the Executor and Coordinator profiles and choose whether Freeflow may manage model state for this repository.",
804
+ : "Configure Coordinator, Helper, and Executor profiles plus the workers available for delegation.",
665
805
  kind: "group",
666
806
  value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
667
807
  inactive: freeflowInactive,
668
808
  displaySuffix: cognitiveRoutingStatus,
669
- children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles, cognitiveRoutingProjectionItem],
809
+ children: [
810
+ cognitiveRoutingEnabledItem,
811
+ cognitiveRoutingDelegationItem,
812
+ ...cognitiveRoutingProfiles,
813
+ cognitiveRoutingProjectionItem,
814
+ ],
670
815
  };
671
816
  })();
672
817
  return [
@@ -688,6 +833,7 @@ function pruneKnownDefaults(config) {
688
833
  { path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
689
834
  { path: ["contextVirtualization"], value: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED },
690
835
  { path: ["conversationHistory"], value: DEFAULT_CONVERSATION_HISTORY_ENABLED },
836
+ { path: ["cognitiveRouting", "delegation"], value: "executor" },
691
837
  ];
692
838
  for (const item of defaultPaths) {
693
839
  if (valuesEqual(getPath(config, item.path), item.value)) {
@@ -761,7 +907,7 @@ async function updateConfig(cwd, item, value, scope = item.configScope ?? "repos
761
907
  await writeFile(join(cwd, ".freeflow/config.json"), `${JSON.stringify(next, null, 2)}\n`, "utf8");
762
908
  }
763
909
  function isCognitiveRoutingProfileItem(item) {
764
- return item.id === "freeflow.cognitiveRouting.executor" || item.id === "freeflow.cognitiveRouting.coordinator";
910
+ return ["coordinator", "helper", "executor"].some((profile) => item.id === `freeflow.cognitiveRouting.${profile}`);
765
911
  }
766
912
  function valueForDisplay(item) {
767
913
  let value;
@@ -801,10 +947,11 @@ function refreshSettingsDerivedState(items) {
801
947
  const freeflowInactive = freeflowItem ? effectiveItemValue(freeflowItem) !== true : false;
802
948
  const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
803
949
  const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
804
- const cognitiveRoutingProfiles = [
805
- findSettingsItem(items, "freeflow.cognitiveRouting.executor"),
806
- findSettingsItem(items, "freeflow.cognitiveRouting.coordinator"),
807
- ];
950
+ const cognitiveRoutingDelegationItem = findSettingsItem(items, "freeflow.cognitiveRouting.delegation");
951
+ const delegation = cognitiveRoutingDelegationItem ? effectiveItemValue(cognitiveRoutingDelegationItem) : "executor";
952
+ const cognitiveRoutingProfiles = ["coordinator", ...workersForDelegation(delegation)].map((profile) =>
953
+ findSettingsItem(items, `freeflow.cognitiveRouting.${profile}`),
954
+ );
808
955
  if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
809
956
  const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
810
957
  const profilesConfigured = cognitiveRoutingProfiles.every((item) =>
@@ -1079,7 +1226,7 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiv
1079
1226
  let reconcileCognitiveRouting = settingsScope === "session";
1080
1227
  const items =
1081
1228
  settingsScope === "session"
1082
- ? sessionFreeflowItems(state, cognitiveRoutingController)
1229
+ ? sessionFreeflowItems(state, cognitiveRoutingController, ctx)
1083
1230
  : freeflowItems(raw, {
1084
1231
  scope: settingsScope,
1085
1232
  layers,
@@ -1122,8 +1269,61 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiv
1122
1269
  return { changed: true, reloadRequired: false };
1123
1270
  }
1124
1271
  if (item.id === "freeflow.session.reset") {
1125
- const result = await resetSessionOverrides(ctx, pi);
1126
- return { changed: result.changed, reloadRequired: result.reloadRequired };
1272
+ const routingResult = cognitiveRoutingController
1273
+ ? await cognitiveRoutingController.resetSessionProfileOverrides("Reset session overrides")
1274
+ : { status: "unchanged" };
1275
+ if (routingResult.status === "blocked" || routingResult.reason) {
1276
+ ctx.ui.notify(
1277
+ `Cognitive Routing session presets could not be reset: ${routingResult.reason ?? routingResult.status}.`,
1278
+ "warning",
1279
+ );
1280
+ return { changed: false, reloadRequired: false };
1281
+ }
1282
+ try {
1283
+ const result = await resetSessionOverrides(ctx, pi);
1284
+ return {
1285
+ changed: result.changed || routingResult.status !== "unchanged",
1286
+ reloadRequired: result.reloadRequired,
1287
+ };
1288
+ } catch (error) {
1289
+ ctx.ui.notify(
1290
+ `Routing presets were reset, but core/context session overrides could not be reset: ${error instanceof Error ? error.message : String(error)}`,
1291
+ "warning",
1292
+ );
1293
+ return {
1294
+ changed: routingResult.status !== "unchanged",
1295
+ reloadRequired: false,
1296
+ };
1297
+ }
1298
+ }
1299
+ if (item.configScope === "session" && isCognitiveRoutingProfileItem(item)) {
1300
+ if (!cognitiveRoutingController) {
1301
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1302
+ return { changed: false, reloadRequired: false };
1303
+ }
1304
+ const profile = item.id.split(".").at(-1);
1305
+ const override =
1306
+ value === undefined || value === LOCAL_INHERIT
1307
+ ? null
1308
+ : {
1309
+ provider: value.provider,
1310
+ modelId: value.model,
1311
+ thinking: value.thinking,
1312
+ };
1313
+ const result = await cognitiveRoutingController.setSessionProfileOverride(
1314
+ profile,
1315
+ override,
1316
+ "Session profile settings",
1317
+ );
1318
+ if (result.status === "blocked" || result.reason) {
1319
+ ctx.ui.notify(
1320
+ `Cognitive Routing session preset could not be applied: ${result.reason ?? result.status}.`,
1321
+ "warning",
1322
+ );
1323
+ return { changed: false, reloadRequired: false };
1324
+ }
1325
+ await afterChange(false);
1326
+ return { changed: result.status !== "unchanged", reloadRequired: false };
1127
1327
  }
1128
1328
  if (item.configScope === "session") {
1129
1329
  const keyById = {
@@ -1,6 +1,18 @@
1
- import { EFFORTS, PROFILES, canonical, isObject, type Effort, type Pair, type Profile } from "./types.js";
1
+ import {
2
+ DELEGATION_MODES,
3
+ EFFORTS,
4
+ PROFILES,
5
+ canonical,
6
+ isObject,
7
+ workersForDelegation,
8
+ type DelegationMode,
9
+ type Effort,
10
+ type Pair,
11
+ type Profile,
12
+ } from "./types.js";
2
13
  export type CognitiveRoutingProfileName = Profile;
3
14
  export type CognitiveRoutingThinkingLevel = Effort;
15
+ export type CognitiveRoutingDelegationMode = DelegationMode;
4
16
  export interface CognitiveRoutingProfile {
5
17
  provider: string;
6
18
  model: string;
@@ -12,6 +24,8 @@ export interface CognitiveRoutingCapabilityState {
12
24
  enabled: boolean;
13
25
  effective: boolean;
14
26
  enabledSource: string;
27
+ delegation: DelegationMode;
28
+ delegationSource: string;
15
29
  projection: boolean;
16
30
  projectionSource: string;
17
31
  profiles: Partial<Record<Profile, CognitiveRoutingProfile>>;
@@ -27,24 +41,29 @@ export const pairFromProfile = (profile: CognitiveRoutingProfile): Pair => ({
27
41
  });
28
42
  function parseLayer(raw: any): any {
29
43
  if (raw === undefined) return {};
30
- if (!isObject(raw) || Object.keys(raw).some((k) => !["enabled", "projection", "profiles"].includes(k)))
44
+ if (
45
+ !isObject(raw) ||
46
+ Object.keys(raw).some((key) => !["enabled", "delegation", "projection", "profiles"].includes(key))
47
+ )
31
48
  throw new Error(
32
- "Routing requires the new enabled/projection/profiles schema; experimental legacy configuration is not migrated.",
49
+ "Routing requires the enabled/delegation/projection/profiles schema; experimental legacy configuration is not migrated.",
33
50
  );
34
51
  for (const key of ["enabled", "projection"])
35
52
  if (raw[key] !== undefined && typeof raw[key] !== "boolean") throw new Error(`${key} must be boolean.`);
53
+ if (raw.delegation !== undefined && !DELEGATION_MODES.includes(raw.delegation))
54
+ throw new Error("delegation must be executor, helper, or both.");
36
55
  if (raw.profiles !== undefined) {
37
- if (!isObject(raw.profiles) || Object.keys(raw.profiles).some((k) => !PROFILES.includes(k as Profile)))
38
- throw new Error("Profiles must be coordinator/executor.");
39
- for (const p of Object.values(raw.profiles) as any[]) {
56
+ if (!isObject(raw.profiles) || Object.keys(raw.profiles).some((key) => !PROFILES.includes(key as Profile)))
57
+ throw new Error("Profiles must be coordinator, helper, or executor.");
58
+ for (const profile of Object.values(raw.profiles) as any[]) {
40
59
  if (
41
- !isObject(p) ||
42
- Object.keys(p).some((k) => !["provider", "model", "thinking"].includes(k)) ||
43
- typeof p.provider !== "string" ||
44
- !p.provider.trim() ||
45
- typeof p.model !== "string" ||
46
- !p.model.trim() ||
47
- !EFFORTS.includes(p.thinking)
60
+ !isObject(profile) ||
61
+ Object.keys(profile).some((key) => !["provider", "model", "thinking"].includes(key)) ||
62
+ typeof profile.provider !== "string" ||
63
+ !profile.provider.trim() ||
64
+ typeof profile.model !== "string" ||
65
+ !profile.model.trim() ||
66
+ !EFFORTS.includes(profile.thinking)
48
67
  )
49
68
  throw new Error("Each profile requires provider, model, and supported thinking.");
50
69
  }
@@ -62,6 +81,8 @@ export async function resolveCognitiveRoutingState(
62
81
  enabled: false,
63
82
  effective: false,
64
83
  enabledSource: "default",
84
+ delegation: "executor",
85
+ delegationSource: "default",
65
86
  projection: false,
66
87
  projectionSource: "default",
67
88
  profiles: {},
@@ -78,6 +99,10 @@ export async function resolveCognitiveRoutingState(
78
99
  state.enabled = layer.enabled;
79
100
  state.enabledSource = source;
80
101
  }
102
+ if (layer.delegation !== undefined) {
103
+ state.delegation = layer.delegation;
104
+ state.delegationSource = source;
105
+ }
81
106
  if (layer.projection !== undefined) {
82
107
  state.projection = layer.projection;
83
108
  state.projectionSource = source;
@@ -101,7 +126,8 @@ export async function resolveCognitiveRoutingState(
101
126
  ...state,
102
127
  blockingReason: { code: "host_unsupported", message: "Required public Pi model registry is unavailable." },
103
128
  };
104
- for (const name of PROFILES) {
129
+ const requiredProfiles: Profile[] = ["coordinator", ...workersForDelegation(state.delegation)];
130
+ for (const name of requiredProfiles) {
105
131
  const profile = state.profiles[name];
106
132
  if (!profile)
107
133
  return { ...state, blockingReason: { code: "profile_missing", message: `Configure the ${name} profile.` } };
@@ -134,10 +160,14 @@ export async function resolveCognitiveRoutingState(
134
160
  };
135
161
  }
136
162
  }
137
- if (canonical(state.profiles.coordinator) === canonical(state.profiles.executor))
138
- return {
139
- ...state,
140
- blockingReason: { code: "profiles_identical", message: "Configure distinct effective model/effort pairs." },
141
- };
163
+ for (const worker of workersForDelegation(state.delegation))
164
+ if (canonical(state.profiles.coordinator) === canonical(state.profiles[worker]))
165
+ return {
166
+ ...state,
167
+ blockingReason: {
168
+ code: "profiles_identical",
169
+ message: `Configure distinct Coordinator and ${worker} model/effort pairs.`,
170
+ },
171
+ };
142
172
  return { ...state, effective: true, blockingReason: { code: "", message: "" } };
143
173
  }