@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.
- 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/README.md +147 -127
- package/capabilities/cognitive-routing/SKILL.md +162 -93
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
- package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
- package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
- package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
- package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
- package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +7 -6
- package/pi-extension/dist/index.js +36 -15
- package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
- package/pi-extension/dist/provider-support/astra/history.js +125 -0
- package/pi-extension/dist/provider-support/astra/index.js +11 -0
- package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
- package/pi-extension/dist/provider-support/index.js +4 -0
- package/pi-extension/dist/runtime/request-history.js +216 -0
- package/pi-extension/dist/runtime/runtime-context.js +29 -10
- package/pi-extension/dist/session-sources/events.js +22 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/sources.js +51 -7
- package/pi-extension/dist/settings/settings-ui.js +247 -47
- package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
- package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
- package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
- package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
- package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
- package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +7 -6
- package/pi-extension/src/index.ts +36 -14
- package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
- package/pi-extension/src/provider-support/astra/history.ts +145 -0
- package/pi-extension/src/provider-support/astra/index.ts +12 -0
- package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
- package/pi-extension/src/provider-support/index.ts +5 -0
- package/pi-extension/src/runtime/request-history.ts +243 -0
- package/pi-extension/src/runtime/runtime-context.ts +32 -10
- package/pi-extension/src/session-sources/events.ts +22 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/sources.ts +77 -6
- package/pi-extension/src/settings/settings-ui.ts +276 -46
- package/pi-extension/tests/cache-reuse/history.test.js +117 -0
- package/pi-extension/tests/cache-reuse/native.test.js +91 -0
- package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
- package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +4 -1
- package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
- package/pi-extension/tests/integration/pi-extension.test.js +508 -29
- package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
- package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
- package/pi-extension/tests/provider-support/astra.test.js +306 -0
- package/pi-extension/tests/provider-support/cli.test.js +92 -0
- package/pi-extension/tests/provider-support/native.test.js +183 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/skills/design-for-depth/SKILL.md +1 -1
- package/skills/design-for-depth/references/implementation-decisions.md +3 -3
- package/skills/discuss/SKILL.md +2 -2
|
@@ -22,10 +22,12 @@ import {
|
|
|
22
22
|
import { isPiFlowHost } from "../runtime/runtime-identity.js";
|
|
23
23
|
import type {
|
|
24
24
|
CognitiveRoutingCapabilityState,
|
|
25
|
+
CognitiveRoutingDelegationMode,
|
|
25
26
|
CognitiveRoutingProfile,
|
|
26
27
|
CognitiveRoutingProfileName,
|
|
27
28
|
CognitiveRoutingThinkingLevel,
|
|
28
29
|
} from "../cognitive-routing-v2/config.js";
|
|
30
|
+
import { isWorkerProfile, workersForDelegation } from "../cognitive-routing-v2/types.js";
|
|
29
31
|
|
|
30
32
|
const DEFAULT_FREEFLOW_ENABLED = true;
|
|
31
33
|
const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
|
|
@@ -67,13 +69,26 @@ type SettingsItem = {
|
|
|
67
69
|
transient?: boolean;
|
|
68
70
|
};
|
|
69
71
|
|
|
72
|
+
type SessionRoutingPair = {
|
|
73
|
+
provider: string;
|
|
74
|
+
modelId: string;
|
|
75
|
+
thinking: CognitiveRoutingThinkingLevel;
|
|
76
|
+
};
|
|
77
|
+
|
|
70
78
|
type CognitiveRoutingSettingsController = {
|
|
71
79
|
state(): { effective: boolean; controlMode: string; activeProfile?: string };
|
|
80
|
+
sessionProfileOverrides(): Partial<Record<CognitiveRoutingProfileName, SessionRoutingPair>>;
|
|
72
81
|
setManualProfile(
|
|
73
|
-
profile:
|
|
82
|
+
profile: CognitiveRoutingProfileName,
|
|
74
83
|
mechanism?: string,
|
|
75
84
|
): Promise<{ status: string; reason?: string }>;
|
|
76
85
|
setAutomaticControl(mechanism?: string): Promise<{ status: string; reason?: string }>;
|
|
86
|
+
setSessionProfileOverride(
|
|
87
|
+
profile: CognitiveRoutingProfileName,
|
|
88
|
+
override: SessionRoutingPair | null,
|
|
89
|
+
mechanism?: string,
|
|
90
|
+
): Promise<{ status: string; reason?: string }>;
|
|
91
|
+
resetSessionProfileOverrides(mechanism?: string): Promise<{ status: string; reason?: string }>;
|
|
77
92
|
};
|
|
78
93
|
|
|
79
94
|
type AfterChangeOptions = {
|
|
@@ -293,6 +308,79 @@ function createScopedBooleanItem(options: ScopedBooleanItemOptions): SettingsIte
|
|
|
293
308
|
return item;
|
|
294
309
|
}
|
|
295
310
|
|
|
311
|
+
function createScopedDelegationItem(options: {
|
|
312
|
+
scope: Exclude<ConfigScope, "session">;
|
|
313
|
+
rawConfig: Record<string, unknown>;
|
|
314
|
+
localConfig: Record<string, unknown>;
|
|
315
|
+
effectiveValue: CognitiveRoutingDelegationMode;
|
|
316
|
+
effectiveSource: ConfigSource;
|
|
317
|
+
}): SettingsItem {
|
|
318
|
+
const path = ["cognitiveRouting", "delegation"];
|
|
319
|
+
const repositoryValue = getPath(options.rawConfig, path);
|
|
320
|
+
const inheritedValue: CognitiveRoutingDelegationMode = ["executor", "helper", "both"].includes(
|
|
321
|
+
repositoryValue as string,
|
|
322
|
+
)
|
|
323
|
+
? (repositoryValue as CognitiveRoutingDelegationMode)
|
|
324
|
+
: "executor";
|
|
325
|
+
const inheritedSource: ConfigSource = repositoryValue === undefined ? "builtin" : "repository";
|
|
326
|
+
const localValue = getPath(options.localConfig, path);
|
|
327
|
+
const descriptions = {
|
|
328
|
+
executor: "Coordinator delegates assignments to Executor.",
|
|
329
|
+
helper: "Coordinator delegates supporting assignments to Helper and keeps production implementation.",
|
|
330
|
+
both: "Coordinator chooses Helper or Executor for each assignment.",
|
|
331
|
+
};
|
|
332
|
+
const item: SettingsItem =
|
|
333
|
+
options.scope === "local"
|
|
334
|
+
? {
|
|
335
|
+
id: "freeflow.cognitiveRouting.delegation",
|
|
336
|
+
label: "Delegation method",
|
|
337
|
+
description:
|
|
338
|
+
"Choose enabled workers for automatic routing. Inherit uses the repository value; this setting does not rank model presets.",
|
|
339
|
+
path,
|
|
340
|
+
kind: "enum",
|
|
341
|
+
value: ["executor", "helper", "both"].includes(localValue as string) ? localValue : LOCAL_INHERIT,
|
|
342
|
+
values: [LOCAL_INHERIT, "executor", "helper", "both"],
|
|
343
|
+
valueLabels: {
|
|
344
|
+
inherit: "Inherit repository",
|
|
345
|
+
executor: "Executor only",
|
|
346
|
+
helper: "Helper only",
|
|
347
|
+
both: "Helper and Executor",
|
|
348
|
+
},
|
|
349
|
+
valueDescriptions: {
|
|
350
|
+
inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
|
|
351
|
+
...descriptions,
|
|
352
|
+
},
|
|
353
|
+
configScope: "local",
|
|
354
|
+
configValues: { inherit: undefined, executor: "executor", helper: "helper", both: "both" },
|
|
355
|
+
effectiveValue: options.effectiveValue,
|
|
356
|
+
effectiveSource: options.effectiveSource,
|
|
357
|
+
inheritedValue,
|
|
358
|
+
inheritedSource,
|
|
359
|
+
}
|
|
360
|
+
: {
|
|
361
|
+
id: "freeflow.cognitiveRouting.delegation",
|
|
362
|
+
label: "Delegation method",
|
|
363
|
+
description: "Choose enabled workers for automatic routing. This edits shared .freeflow/config.json.",
|
|
364
|
+
path,
|
|
365
|
+
kind: "enum",
|
|
366
|
+
value: inheritedValue,
|
|
367
|
+
values: ["executor", "helper", "both"],
|
|
368
|
+
valueLabels: {
|
|
369
|
+
executor: "Executor only",
|
|
370
|
+
helper: "Helper only",
|
|
371
|
+
both: "Helper and Executor",
|
|
372
|
+
},
|
|
373
|
+
valueDescriptions: descriptions,
|
|
374
|
+
defaultValue: "executor",
|
|
375
|
+
configScope: "repository",
|
|
376
|
+
effectiveValue: options.effectiveValue,
|
|
377
|
+
effectiveSource: options.effectiveSource,
|
|
378
|
+
localOverrideValue: ["executor", "helper", "both"].includes(localValue as string) ? localValue : undefined,
|
|
379
|
+
};
|
|
380
|
+
item.displaySuffix = coreDisplaySuffix(item);
|
|
381
|
+
return item;
|
|
382
|
+
}
|
|
383
|
+
|
|
296
384
|
function resolveSettingsCoreView(
|
|
297
385
|
rawConfig: Record<string, unknown>,
|
|
298
386
|
layers?: Awaited<ReturnType<typeof readFreeflowConfigLayers>>,
|
|
@@ -371,6 +459,7 @@ function createSessionBooleanItem(options: {
|
|
|
371
459
|
function sessionFreeflowItems(
|
|
372
460
|
state: Awaited<ReturnType<typeof readCapabilityState>>,
|
|
373
461
|
cognitiveRoutingController?: CognitiveRoutingSettingsController,
|
|
462
|
+
ctx?: any,
|
|
374
463
|
): SettingsItem[] {
|
|
375
464
|
const sessionOverrides = state.sessionOverrides as Record<string, boolean>;
|
|
376
465
|
const configured = state.configuredCoreConfig;
|
|
@@ -424,48 +513,79 @@ function sessionFreeflowItems(
|
|
|
424
513
|
conversationHistoryItem.inactive = freeflowInactive;
|
|
425
514
|
|
|
426
515
|
const cognitiveRoutingState = cognitiveRoutingController?.state();
|
|
516
|
+
const delegation = state.cognitiveRouting?.delegation ?? "executor";
|
|
517
|
+
const heldProfile = cognitiveRoutingState?.controlMode.startsWith("manual-")
|
|
518
|
+
? cognitiveRoutingState.activeProfile
|
|
519
|
+
: undefined;
|
|
520
|
+
const availableProfiles: CognitiveRoutingProfileName[] = ["coordinator", ...workersForDelegation(delegation)];
|
|
521
|
+
if (heldProfile === "coordinator" || isWorkerProfile(heldProfile)) availableProfiles.push(heldProfile);
|
|
522
|
+
const manualProfiles = [...new Set(availableProfiles)];
|
|
427
523
|
const cognitiveRoutingProfile =
|
|
428
|
-
|
|
429
|
-
? "executor"
|
|
430
|
-
: cognitiveRoutingState?.controlMode === "manual-coordinator"
|
|
431
|
-
? "coordinator"
|
|
432
|
-
: "auto";
|
|
524
|
+
heldProfile && manualProfiles.includes(heldProfile as CognitiveRoutingProfileName) ? heldProfile : "auto";
|
|
433
525
|
const cognitiveRoutingItem: SettingsItem | undefined = state.cognitiveRouting
|
|
434
526
|
? {
|
|
435
527
|
id: "freeflow.cognitiveRouting.profile",
|
|
436
528
|
label: "Cognitive Routing",
|
|
437
|
-
description: "Hold
|
|
529
|
+
description: "Hold an enabled profile manually or release the hold for automatic model control.",
|
|
438
530
|
kind: "enum",
|
|
439
531
|
value: cognitiveRoutingProfile,
|
|
440
|
-
values: ["auto",
|
|
441
|
-
valueLabels:
|
|
442
|
-
auto
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
coordinator: "Hold the coordinator profile until /freeflow profile auto.",
|
|
450
|
-
},
|
|
532
|
+
values: ["auto", ...manualProfiles],
|
|
533
|
+
valueLabels: Object.fromEntries([
|
|
534
|
+
["auto", "automatic"],
|
|
535
|
+
...manualProfiles.map((profile) => [profile, `manual · ${profile}`]),
|
|
536
|
+
]),
|
|
537
|
+
valueDescriptions: Object.fromEntries([
|
|
538
|
+
["auto", "Release the manual hold and return to Coordinator reconciliation."],
|
|
539
|
+
...manualProfiles.map((profile) => [profile, `Hold the ${profile} profile until /freeflow profile auto.`]),
|
|
540
|
+
]),
|
|
451
541
|
inactive: freeflowInactive || !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
|
|
452
542
|
runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
|
|
453
543
|
displaySuffix: cognitiveRoutingState?.effective ? cognitiveRoutingProfile : "unavailable",
|
|
454
544
|
}
|
|
455
545
|
: undefined;
|
|
456
546
|
|
|
547
|
+
const sessionProfiles = cognitiveRoutingController?.sessionProfileOverrides();
|
|
548
|
+
const profileItems =
|
|
549
|
+
state.cognitiveRouting && cognitiveRoutingController
|
|
550
|
+
? manualProfiles.map((name) =>
|
|
551
|
+
cognitiveRoutingProfileItem({
|
|
552
|
+
name,
|
|
553
|
+
scope: "session",
|
|
554
|
+
rawConfig: {},
|
|
555
|
+
localConfig: {},
|
|
556
|
+
capabilityState: state.cognitiveRouting as CognitiveRoutingCapabilityState,
|
|
557
|
+
ctx,
|
|
558
|
+
sessionProfiles,
|
|
559
|
+
}),
|
|
560
|
+
)
|
|
561
|
+
: [];
|
|
562
|
+
const cognitiveRoutingPresets: SettingsItem | undefined = profileItems.length
|
|
563
|
+
? {
|
|
564
|
+
id: "freeflow.cognitiveRouting.presets",
|
|
565
|
+
label: "Cognitive Routing presets",
|
|
566
|
+
description: "Temporarily choose complete model/effort pairs for profiles enabled in this Pi session.",
|
|
567
|
+
kind: "group",
|
|
568
|
+
value: profileItems.some((item) => item.value !== LOCAL_INHERIT && item.value !== undefined),
|
|
569
|
+
displaySuffix: `${profileItems.filter((item) => item.value !== LOCAL_INHERIT && item.value !== undefined).length}/${profileItems.length} session overrides`,
|
|
570
|
+
inactive: freeflowInactive || !state.cognitiveRouting?.effective || cognitiveRoutingController === undefined,
|
|
571
|
+
children: profileItems,
|
|
572
|
+
}
|
|
573
|
+
: undefined;
|
|
574
|
+
|
|
457
575
|
return [
|
|
458
576
|
freeflowItem,
|
|
459
577
|
...(cognitiveRoutingItem ? [cognitiveRoutingItem] : []),
|
|
578
|
+
...(cognitiveRoutingPresets ? [cognitiveRoutingPresets] : []),
|
|
460
579
|
{
|
|
461
580
|
id: "freeflow.session.reset",
|
|
462
581
|
label: "Reset session overrides",
|
|
463
|
-
description:
|
|
582
|
+
description:
|
|
583
|
+
"Clear Freeflow, Context Virtualization, Conversation History, and routing profile overrides for this Pi session.",
|
|
464
584
|
kind: "enum",
|
|
465
585
|
value: "available",
|
|
466
586
|
values: ["reset"],
|
|
467
587
|
valueLabels: { reset: "Reset all session overrides" },
|
|
468
|
-
valueDescriptions: { reset: "Return every session setting to its configured value." },
|
|
588
|
+
valueDescriptions: { reset: "Return every session setting and routing preset to its configured value." },
|
|
469
589
|
format: () => "available",
|
|
470
590
|
transient: true,
|
|
471
591
|
},
|
|
@@ -574,6 +694,7 @@ function cognitiveRoutingProfileDisplay(value: unknown): string {
|
|
|
574
694
|
}
|
|
575
695
|
|
|
576
696
|
function cognitiveRoutingSettingsSource(source: unknown): ConfigSource {
|
|
697
|
+
if (source === "session") return "session";
|
|
577
698
|
if (source === "repository") return "repository";
|
|
578
699
|
if (source === "personal" || source === "local") return "local";
|
|
579
700
|
return "builtin";
|
|
@@ -581,6 +702,7 @@ function cognitiveRoutingSettingsSource(source: unknown): ConfigSource {
|
|
|
581
702
|
|
|
582
703
|
function cognitiveRoutingProfileDisplaySuffix(source: unknown, scope: ConfigScope): string | undefined {
|
|
583
704
|
if (!source) return undefined;
|
|
705
|
+
if (source === "session") return "(session override)";
|
|
584
706
|
if (source === "personal" && scope === "repository") return "(effective personal)";
|
|
585
707
|
return `(${source})`;
|
|
586
708
|
}
|
|
@@ -605,6 +727,11 @@ function createCognitiveRoutingProfileWizard(
|
|
|
605
727
|
currentValue: unknown,
|
|
606
728
|
models: CognitiveRoutingModelOption[],
|
|
607
729
|
allowInherit: boolean,
|
|
730
|
+
inheritChoice: { label: string; description: string; summary: string } = {
|
|
731
|
+
label: "Inherit repository preset",
|
|
732
|
+
description: "Remove the personal override without changing the repository preset.",
|
|
733
|
+
summary: "inherit repository preset",
|
|
734
|
+
},
|
|
608
735
|
): SettingsWizard {
|
|
609
736
|
const currentProfile = isCognitiveRoutingProfile(currentValue) ? currentValue : undefined;
|
|
610
737
|
const currentModelKey = currentProfile
|
|
@@ -616,8 +743,8 @@ function createCognitiveRoutingProfileWizard(
|
|
|
616
743
|
{
|
|
617
744
|
key: LOCAL_INHERIT,
|
|
618
745
|
value: LOCAL_INHERIT,
|
|
619
|
-
label:
|
|
620
|
-
description:
|
|
746
|
+
label: inheritChoice.label,
|
|
747
|
+
description: inheritChoice.description,
|
|
621
748
|
},
|
|
622
749
|
]
|
|
623
750
|
: []),
|
|
@@ -652,7 +779,7 @@ function createCognitiveRoutingProfileWizard(
|
|
|
652
779
|
if (selectedValues.length === 1) {
|
|
653
780
|
const selected = selectedValues[0];
|
|
654
781
|
if (selected === LOCAL_INHERIT) {
|
|
655
|
-
return cognitiveRoutingConfirmStep(
|
|
782
|
+
return cognitiveRoutingConfirmStep(inheritChoice.summary);
|
|
656
783
|
}
|
|
657
784
|
const model = selected as CognitiveRoutingModelOption;
|
|
658
785
|
if (!model?.key || model.thinkingLevels.length === 0) return undefined;
|
|
@@ -695,36 +822,65 @@ function cognitiveRoutingProfileItem(options: {
|
|
|
695
822
|
localConfig: Record<string, unknown>;
|
|
696
823
|
capabilityState: CognitiveRoutingCapabilityState | undefined;
|
|
697
824
|
ctx: any;
|
|
825
|
+
sessionProfiles?: Partial<Record<CognitiveRoutingProfileName, SessionRoutingPair>>;
|
|
698
826
|
}): SettingsItem {
|
|
699
827
|
const path = ["cognitiveRouting", "profiles", options.name];
|
|
700
828
|
const repositoryProfile = getPath(options.rawConfig, path);
|
|
701
829
|
const localProfile = getPath(options.localConfig, path);
|
|
702
|
-
const
|
|
830
|
+
const configuredProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
|
|
831
|
+
const sessionPair = options.sessionProfiles?.[options.name];
|
|
832
|
+
const sessionProfile = sessionPair
|
|
833
|
+
? { provider: sessionPair.provider, model: sessionPair.modelId, thinking: sessionPair.thinking }
|
|
834
|
+
: undefined;
|
|
835
|
+
const effectiveProfile = sessionProfile ?? configuredProfile;
|
|
703
836
|
const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
|
|
704
|
-
const value =
|
|
705
|
-
|
|
837
|
+
const value =
|
|
838
|
+
options.scope === "session"
|
|
839
|
+
? (sessionProfile ?? LOCAL_INHERIT)
|
|
840
|
+
: options.scope === "local"
|
|
841
|
+
? localValue
|
|
842
|
+
: repositoryProfile;
|
|
843
|
+
const configuredSource =
|
|
706
844
|
options.capabilityState?.profileSources?.[options.name] ??
|
|
707
845
|
(isCognitiveRoutingProfile(repositoryProfile) ? "repository" : undefined);
|
|
846
|
+
const source = options.scope === "session" ? (sessionProfile ? "session" : configuredSource) : configuredSource;
|
|
708
847
|
const models = cognitiveRoutingModelOptions(options.ctx);
|
|
709
|
-
const inactive = options.scope
|
|
848
|
+
const inactive = options.scope !== "repository" && models.length === 0 && value !== LOCAL_INHERIT;
|
|
849
|
+
const inheritChoice =
|
|
850
|
+
options.scope === "session"
|
|
851
|
+
? {
|
|
852
|
+
label: "Inherit configured preset",
|
|
853
|
+
description: "Remove the session override without changing local or repository configuration.",
|
|
854
|
+
summary: "inherit configured preset",
|
|
855
|
+
}
|
|
856
|
+
: undefined;
|
|
710
857
|
|
|
711
858
|
return {
|
|
712
859
|
id: `freeflow.cognitiveRouting.${options.name}`,
|
|
713
860
|
label: `${options.name.charAt(0).toUpperCase()}${options.name.slice(1)} preset`,
|
|
714
861
|
description:
|
|
715
|
-
|
|
862
|
+
options.scope === "session"
|
|
863
|
+
? "Temporarily choose the model and effort for this profile in the current Pi session only."
|
|
864
|
+
: "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
|
|
716
865
|
path,
|
|
717
866
|
kind: "string",
|
|
718
867
|
value,
|
|
719
|
-
format:
|
|
868
|
+
format: (current) => {
|
|
869
|
+
const inherits = current === LOCAL_INHERIT || (options.scope === "session" && current === undefined);
|
|
870
|
+
return inherits
|
|
871
|
+
? options.scope === "session"
|
|
872
|
+
? "inherit configured preset"
|
|
873
|
+
: "inherit repository preset"
|
|
874
|
+
: cognitiveRoutingProfileDisplay(current);
|
|
875
|
+
},
|
|
720
876
|
configScope: options.scope,
|
|
721
877
|
effectiveValue: effectiveProfile,
|
|
722
878
|
effectiveSource: cognitiveRoutingSettingsSource(source),
|
|
723
|
-
inheritedValue: repositoryProfile,
|
|
724
|
-
inheritedSource: "
|
|
879
|
+
inheritedValue: options.scope === "session" ? configuredProfile : repositoryProfile,
|
|
880
|
+
inheritedSource: cognitiveRoutingSettingsSource(configuredSource ?? "builtin"),
|
|
725
881
|
inactive,
|
|
726
882
|
displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
|
|
727
|
-
wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope
|
|
883
|
+
wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope !== "repository", inheritChoice),
|
|
728
884
|
};
|
|
729
885
|
}
|
|
730
886
|
|
|
@@ -747,7 +903,7 @@ function freeflowItems(
|
|
|
747
903
|
runtimeAvailable?: boolean;
|
|
748
904
|
} = {},
|
|
749
905
|
): SettingsItem[] {
|
|
750
|
-
const scope = options.scope
|
|
906
|
+
const scope: Exclude<ConfigScope, "session"> = options.scope === "local" ? "local" : "repository";
|
|
751
907
|
const layers = options.layers;
|
|
752
908
|
const { localConfig, core, sources } = resolveSettingsCoreView(rawConfig, layers);
|
|
753
909
|
|
|
@@ -812,6 +968,16 @@ function freeflowItems(
|
|
|
812
968
|
cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
|
|
813
969
|
cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
|
|
814
970
|
|
|
971
|
+
const cognitiveRoutingDelegationItem = createScopedDelegationItem({
|
|
972
|
+
scope,
|
|
973
|
+
rawConfig,
|
|
974
|
+
localConfig,
|
|
975
|
+
effectiveValue: cognitiveRoutingState?.delegation ?? "executor",
|
|
976
|
+
effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.delegationSource),
|
|
977
|
+
});
|
|
978
|
+
cognitiveRoutingDelegationItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
|
|
979
|
+
cognitiveRoutingDelegationItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
|
|
980
|
+
|
|
815
981
|
const cognitiveRoutingProjectionItem = createScopedBooleanItem({
|
|
816
982
|
scope,
|
|
817
983
|
rawConfig,
|
|
@@ -819,7 +985,7 @@ function freeflowItems(
|
|
|
819
985
|
id: "freeflow.cognitiveRouting.projection",
|
|
820
986
|
label: "Context projection",
|
|
821
987
|
description:
|
|
822
|
-
"Select
|
|
988
|
+
"Select worker evidence for Coordinator; disable projection to keep ordinary Pi context in all enabled profiles.",
|
|
823
989
|
path: ["cognitiveRouting", "projection"],
|
|
824
990
|
effectiveValue: cognitiveRoutingState?.projection ?? false,
|
|
825
991
|
effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.projectionSource),
|
|
@@ -828,7 +994,7 @@ function freeflowItems(
|
|
|
828
994
|
cognitiveRoutingProjectionItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
|
|
829
995
|
cognitiveRoutingProjectionItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
|
|
830
996
|
|
|
831
|
-
const cognitiveRoutingProfiles = ["
|
|
997
|
+
const cognitiveRoutingProfiles = ["coordinator", "helper", "executor"].map((name) =>
|
|
832
998
|
cognitiveRoutingProfileItem({
|
|
833
999
|
name: name as CognitiveRoutingProfileName,
|
|
834
1000
|
scope,
|
|
@@ -856,12 +1022,17 @@ function freeflowItems(
|
|
|
856
1022
|
label: "Cognitive Routing",
|
|
857
1023
|
description: cognitiveRoutingRuntimeDisabled
|
|
858
1024
|
? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
|
|
859
|
-
: "Configure
|
|
1025
|
+
: "Configure Coordinator, Helper, and Executor profiles plus the workers available for delegation.",
|
|
860
1026
|
kind: "group" as const,
|
|
861
1027
|
value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
|
|
862
1028
|
inactive: freeflowInactive,
|
|
863
1029
|
displaySuffix: cognitiveRoutingStatus,
|
|
864
|
-
children: [
|
|
1030
|
+
children: [
|
|
1031
|
+
cognitiveRoutingEnabledItem,
|
|
1032
|
+
cognitiveRoutingDelegationItem,
|
|
1033
|
+
...cognitiveRoutingProfiles,
|
|
1034
|
+
cognitiveRoutingProjectionItem,
|
|
1035
|
+
],
|
|
865
1036
|
} satisfies SettingsItem;
|
|
866
1037
|
})();
|
|
867
1038
|
|
|
@@ -885,6 +1056,7 @@ function pruneKnownDefaults(config: Record<string, unknown>) {
|
|
|
885
1056
|
{ path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
|
|
886
1057
|
{ path: ["contextVirtualization"], value: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED },
|
|
887
1058
|
{ path: ["conversationHistory"], value: DEFAULT_CONVERSATION_HISTORY_ENABLED },
|
|
1059
|
+
{ path: ["cognitiveRouting", "delegation"], value: "executor" },
|
|
888
1060
|
];
|
|
889
1061
|
|
|
890
1062
|
for (const item of defaultPaths) {
|
|
@@ -971,7 +1143,7 @@ async function updateConfig(
|
|
|
971
1143
|
}
|
|
972
1144
|
|
|
973
1145
|
function isCognitiveRoutingProfileItem(item: SettingsItem): boolean {
|
|
974
|
-
return
|
|
1146
|
+
return ["coordinator", "helper", "executor"].some((profile) => item.id === `freeflow.cognitiveRouting.${profile}`);
|
|
975
1147
|
}
|
|
976
1148
|
|
|
977
1149
|
function valueForDisplay(item: SettingsItem): string {
|
|
@@ -1015,10 +1187,13 @@ function refreshSettingsDerivedState(items: SettingsItem[]) {
|
|
|
1015
1187
|
const freeflowInactive = freeflowItem ? effectiveItemValue(freeflowItem) !== true : false;
|
|
1016
1188
|
const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
|
|
1017
1189
|
const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1190
|
+
const cognitiveRoutingDelegationItem = findSettingsItem(items, "freeflow.cognitiveRouting.delegation");
|
|
1191
|
+
const delegation = (
|
|
1192
|
+
cognitiveRoutingDelegationItem ? effectiveItemValue(cognitiveRoutingDelegationItem) : "executor"
|
|
1193
|
+
) as CognitiveRoutingDelegationMode;
|
|
1194
|
+
const cognitiveRoutingProfiles = ["coordinator", ...workersForDelegation(delegation)].map((profile) =>
|
|
1195
|
+
findSettingsItem(items, `freeflow.cognitiveRouting.${profile}`),
|
|
1196
|
+
);
|
|
1022
1197
|
|
|
1023
1198
|
if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
|
|
1024
1199
|
const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
|
|
@@ -1336,7 +1511,7 @@ export async function handleFreeflowCommand(
|
|
|
1336
1511
|
let reconcileCognitiveRouting = settingsScope === "session";
|
|
1337
1512
|
const items =
|
|
1338
1513
|
settingsScope === "session"
|
|
1339
|
-
? sessionFreeflowItems(state, cognitiveRoutingController)
|
|
1514
|
+
? sessionFreeflowItems(state, cognitiveRoutingController, ctx)
|
|
1340
1515
|
: freeflowItems(raw, {
|
|
1341
1516
|
scope: settingsScope,
|
|
1342
1517
|
layers,
|
|
@@ -1368,7 +1543,7 @@ export async function handleFreeflowCommand(
|
|
|
1368
1543
|
value === "auto"
|
|
1369
1544
|
? await cognitiveRoutingController.setAutomaticControl("profile-settings")
|
|
1370
1545
|
: await cognitiveRoutingController.setManualProfile(
|
|
1371
|
-
value as
|
|
1546
|
+
value as CognitiveRoutingProfileName,
|
|
1372
1547
|
"profile-settings",
|
|
1373
1548
|
);
|
|
1374
1549
|
if ((result.status !== "automatic" && result.status !== "active") || result.reason) {
|
|
@@ -1382,8 +1557,63 @@ export async function handleFreeflowCommand(
|
|
|
1382
1557
|
return { changed: true, reloadRequired: false };
|
|
1383
1558
|
}
|
|
1384
1559
|
if (item.id === "freeflow.session.reset") {
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1560
|
+
const routingResult = cognitiveRoutingController
|
|
1561
|
+
? await cognitiveRoutingController.resetSessionProfileOverrides("Reset session overrides")
|
|
1562
|
+
: { status: "unchanged" };
|
|
1563
|
+
if (routingResult.status === "blocked" || routingResult.reason) {
|
|
1564
|
+
ctx.ui.notify(
|
|
1565
|
+
`Cognitive Routing session presets could not be reset: ${routingResult.reason ?? routingResult.status}.`,
|
|
1566
|
+
"warning",
|
|
1567
|
+
);
|
|
1568
|
+
return { changed: false, reloadRequired: false };
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
const result = await resetSessionOverrides(ctx, pi);
|
|
1572
|
+
return {
|
|
1573
|
+
changed: result.changed || routingResult.status !== "unchanged",
|
|
1574
|
+
reloadRequired: result.reloadRequired,
|
|
1575
|
+
};
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
ctx.ui.notify(
|
|
1578
|
+
`Routing presets were reset, but core/context session overrides could not be reset: ${
|
|
1579
|
+
error instanceof Error ? error.message : String(error)
|
|
1580
|
+
}`,
|
|
1581
|
+
"warning",
|
|
1582
|
+
);
|
|
1583
|
+
return {
|
|
1584
|
+
changed: routingResult.status !== "unchanged",
|
|
1585
|
+
reloadRequired: false,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (item.configScope === "session" && isCognitiveRoutingProfileItem(item)) {
|
|
1590
|
+
if (!cognitiveRoutingController) {
|
|
1591
|
+
ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
|
|
1592
|
+
return { changed: false, reloadRequired: false };
|
|
1593
|
+
}
|
|
1594
|
+
const profile = item.id.split(".").at(-1) as CognitiveRoutingProfileName;
|
|
1595
|
+
const override =
|
|
1596
|
+
value === undefined || value === LOCAL_INHERIT
|
|
1597
|
+
? null
|
|
1598
|
+
: {
|
|
1599
|
+
provider: (value as CognitiveRoutingProfile).provider,
|
|
1600
|
+
modelId: (value as CognitiveRoutingProfile).model,
|
|
1601
|
+
thinking: (value as CognitiveRoutingProfile).thinking,
|
|
1602
|
+
};
|
|
1603
|
+
const result = await cognitiveRoutingController.setSessionProfileOverride(
|
|
1604
|
+
profile,
|
|
1605
|
+
override,
|
|
1606
|
+
"Session profile settings",
|
|
1607
|
+
);
|
|
1608
|
+
if (result.status === "blocked" || result.reason) {
|
|
1609
|
+
ctx.ui.notify(
|
|
1610
|
+
`Cognitive Routing session preset could not be applied: ${result.reason ?? result.status}.`,
|
|
1611
|
+
"warning",
|
|
1612
|
+
);
|
|
1613
|
+
return { changed: false, reloadRequired: false };
|
|
1614
|
+
}
|
|
1615
|
+
await afterChange(false);
|
|
1616
|
+
return { changed: result.status !== "unchanged", reloadRequired: false };
|
|
1387
1617
|
}
|
|
1388
1618
|
if (item.configScope === "session") {
|
|
1389
1619
|
const keyById = {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { RequestHistory } from "../../dist/runtime/request-history.js";
|
|
8
|
+
const state = (text) => ({ role: "custom", customType: "freeflow-runtime-state", content: text, display: false });
|
|
9
|
+
const communication = (text) => ({
|
|
10
|
+
role: "custom",
|
|
11
|
+
customType: "freeflow-routing-communication",
|
|
12
|
+
content: text,
|
|
13
|
+
display: false,
|
|
14
|
+
});
|
|
15
|
+
const user = (text) => ({ role: "user", content: [{ type: "text", text }], timestamp: 1 });
|
|
16
|
+
function fixture(manager = SessionManager.inMemory("/tmp/freeflow-cache")) {
|
|
17
|
+
const pi = { appendEntry: (type, data) => manager.appendCustomEntry(type, data) };
|
|
18
|
+
return { manager, pi, ctx: { sessionManager: manager, ui: { setStatus() {} } }, history: new RequestHistory(pi) };
|
|
19
|
+
}
|
|
20
|
+
test("generated state stays at its original position and changed state appends", async () => {
|
|
21
|
+
const f = fixture(),
|
|
22
|
+
one = await f.history.assemble([state("enabled"), user("one")], "ordinary", f.ctx);
|
|
23
|
+
const two = await f.history.assemble([user("one"), state("disabled"), user("two")], "ordinary", f.ctx);
|
|
24
|
+
assert.deepEqual(two.slice(0, one.length), one);
|
|
25
|
+
assert.equal(two.at(-1).content, "disabled");
|
|
26
|
+
const count = f.manager.getEntries().length;
|
|
27
|
+
assert.deepEqual(await f.history.assemble([state("disabled"), user("one"), user("two")], "ordinary", f.ctx), two);
|
|
28
|
+
assert.equal(f.manager.getEntries().length, count);
|
|
29
|
+
const reload = new RequestHistory(f.pi);
|
|
30
|
+
const three = await reload.assemble([state("disabled"), user("one"), user("two"), user("three")], "ordinary", f.ctx);
|
|
31
|
+
assert.deepEqual(three.slice(0, two.length), two);
|
|
32
|
+
});
|
|
33
|
+
test("view changes never import excluded bodies or stale required communication", async () => {
|
|
34
|
+
const f = fixture();
|
|
35
|
+
await f.history.assemble([user("SECRET"), state("worker"), communication("required report")], "ordinary", f.ctx);
|
|
36
|
+
const coordinator = await f.history.assemble([user("selected"), state("coordinator")], "coordinator", f.ctx);
|
|
37
|
+
assert.ok(!JSON.stringify(coordinator).includes("SECRET"));
|
|
38
|
+
const reduced = await f.history.assemble([user("new visible"), state("worker")], "ordinary", f.ctx);
|
|
39
|
+
assert.ok(!JSON.stringify(reduced).includes("SECRET"));
|
|
40
|
+
assert.ok(!JSON.stringify(reduced).includes("required report"));
|
|
41
|
+
});
|
|
42
|
+
test("context persistence failure returns the current qualified view without old bodies", async () => {
|
|
43
|
+
const f = fixture();
|
|
44
|
+
await f.history.assemble([user("old"), state("first")], "ordinary", f.ctx);
|
|
45
|
+
f.pi.appendEntry = () => {
|
|
46
|
+
throw Error("disk");
|
|
47
|
+
};
|
|
48
|
+
const current = [user("current"), state("second")];
|
|
49
|
+
assert.equal(await f.history.assemble(current, "ordinary", f.ctx), current);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("changing only the view label retains the exact permitted prefix", async () => {
|
|
53
|
+
const f = fixture();
|
|
54
|
+
const first = await f.history.assemble([user("common"), state("ordinary")], "ordinary", f.ctx);
|
|
55
|
+
const next = await f.history.assemble([user("common"), state("coordinator")], "coordinator", f.ctx);
|
|
56
|
+
assert.deepEqual(next.slice(0, first.length), first);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("native labeled forks preserve generated-state positions", async () => {
|
|
60
|
+
const root = await mkdtemp(join(tmpdir(), "freeflow-cache-fork-"));
|
|
61
|
+
try {
|
|
62
|
+
const manager = SessionManager.create(root, root),
|
|
63
|
+
f = fixture(manager);
|
|
64
|
+
const id = manager.appendMessage(user("one"));
|
|
65
|
+
manager.appendLabelChange(id, "named");
|
|
66
|
+
const first = await f.history.assemble([user("one"), state("enabled")], "ordinary", f.ctx);
|
|
67
|
+
manager.appendMessage({
|
|
68
|
+
role: "assistant",
|
|
69
|
+
content: [{ type: "text", text: "done" }],
|
|
70
|
+
timestamp: 2,
|
|
71
|
+
stopReason: "stop",
|
|
72
|
+
});
|
|
73
|
+
const path = manager.createBranchedSession(manager.getLeafId());
|
|
74
|
+
const nextFixture = fixture(SessionManager.open(path));
|
|
75
|
+
const next = await nextFixture.history.assemble(
|
|
76
|
+
[user("one"), user("two"), state("disabled")],
|
|
77
|
+
"ordinary",
|
|
78
|
+
nextFixture.ctx,
|
|
79
|
+
);
|
|
80
|
+
assert.deepEqual(next.slice(0, first.length), first);
|
|
81
|
+
} finally {
|
|
82
|
+
await rm(root, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("budget notices see assembled historical overhead and clear without rewriting old notices", async () => {
|
|
87
|
+
const f = fixture();
|
|
88
|
+
let length;
|
|
89
|
+
const first = await f.history.assemble([user("one"), state("enabled")], "ordinary", f.ctx, (messages) => {
|
|
90
|
+
length = messages.length;
|
|
91
|
+
return { role: "custom", customType: "freeflow-routing-budget", content: "Large input", display: false };
|
|
92
|
+
});
|
|
93
|
+
assert.equal(length, 2);
|
|
94
|
+
const next = await f.history.assemble([user("one"), user("two"), state("enabled")], "ordinary", f.ctx, (messages) => {
|
|
95
|
+
length = messages.length;
|
|
96
|
+
});
|
|
97
|
+
assert.equal(length, 4);
|
|
98
|
+
assert.deepEqual(next.slice(0, first.length), first);
|
|
99
|
+
assert.match(next.at(-1).content, /warning cleared/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("ending a restoration obligation retains already admitted communication until context changes", async () => {
|
|
103
|
+
const f = fixture();
|
|
104
|
+
const first = await f.history.assemble(
|
|
105
|
+
[user("one"), communication("accepted report"), state("active")],
|
|
106
|
+
"ordinary",
|
|
107
|
+
f.ctx,
|
|
108
|
+
);
|
|
109
|
+
const closed = await f.history.assemble([user("one"), user("two"), state("closed")], "ordinary", f.ctx);
|
|
110
|
+
assert.deepEqual(closed.slice(0, first.length), first);
|
|
111
|
+
const again = await f.history.assemble(
|
|
112
|
+
[user("one"), user("two"), communication("accepted report"), state("closed")],
|
|
113
|
+
"ordinary",
|
|
114
|
+
f.ctx,
|
|
115
|
+
);
|
|
116
|
+
assert.equal(again.filter((message) => message.content === "accepted report").length, 1);
|
|
117
|
+
});
|