@frockbot/configuration-core 0.3.8 → 0.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/configuration-core",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,8 +12,8 @@
12
12
  "typecheck": "tsc --noEmit -p tsconfig.json"
13
13
  },
14
14
  "dependencies": {
15
- "@frockbot/connection-core": "0.3.8",
16
- "@frockbot/kernel-composition": "0.3.8"
15
+ "@frockbot/connection-core": "0.3.10",
16
+ "@frockbot/kernel-composition": "0.3.10"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/bun": "1.3.6",
package/src/index.test.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  resolveEffectiveBotModelV1,
27
27
  type ExecutionPackageDefinition,
28
28
  type ModelBindingV1,
29
+ type UserSettingsViewV1,
29
30
  } from "./index.js";
30
31
  import {
31
32
  isApplicationDeploymentHash,
@@ -1657,6 +1658,144 @@ describe("effective Bot model resolution", () => {
1657
1658
  expect(effective.model).toBeUndefined();
1658
1659
  });
1659
1660
 
1661
+ /**
1662
+ * The real account shape: a platform provider that is always there, and a
1663
+ * separate provider Package the User switched on and bound their model to.
1664
+ */
1665
+ const flockModel: ModelBindingV1 = {
1666
+ connectionId: "flock-ai",
1667
+ providerModelId: "@flock/auto",
1668
+ };
1669
+ const flockPackage: ExecutionPackageDefinition = {
1670
+ packageId: "provider-flock-ai",
1671
+ version: "0.0.1",
1672
+ settings: [],
1673
+ capabilities: [
1674
+ {
1675
+ id: "flock-ai-models",
1676
+ kind: "model",
1677
+ connectionTypes: ["flock-ai-account"],
1678
+ },
1679
+ ],
1680
+ connectionTypes: [
1681
+ { id: "flock-ai-account", capabilities: ["flock-ai-models"] },
1682
+ ],
1683
+ };
1684
+ function withFlock(): UserSettingsViewV1 {
1685
+ const base = user();
1686
+ return {
1687
+ ...base,
1688
+ packages: [
1689
+ ...base.packages,
1690
+ {
1691
+ packageId: "provider-flock-ai",
1692
+ version: "0.0.1",
1693
+ state: "installed",
1694
+ },
1695
+ {
1696
+ packageId: "custom-models",
1697
+ version: "0.0.1",
1698
+ state: "installed",
1699
+ values: { model: accountModel },
1700
+ },
1701
+ ],
1702
+ connections: [
1703
+ ...base.connections,
1704
+ {
1705
+ connectionId: "flock-ai",
1706
+ packageId: "provider-flock-ai",
1707
+ connectionTypeId: "flock-ai-account",
1708
+ displayName: "Flock AI",
1709
+ state: "ready",
1710
+ providerType: "flock-ai",
1711
+ modelCatalog: {
1712
+ schemaVersion: 1,
1713
+ generation: "flock-catalog-1",
1714
+ state: "fresh",
1715
+ models: [
1716
+ {
1717
+ providerModelId: "@flock/auto",
1718
+ displayName: "Auto",
1719
+ capabilities: { tools: true, vision: false, reasoning: true },
1720
+ source: "discovered",
1721
+ },
1722
+ ],
1723
+ },
1724
+ safeMetadata: {},
1725
+ },
1726
+ ],
1727
+ platformModel: flockModel,
1728
+ };
1729
+ }
1730
+ const flockPackages = [...modelPackages, flockPackage];
1731
+
1732
+ test("falls back to the platform model when the bound provider is switched off", () => {
1733
+ const providerOff = (() => {
1734
+ const settings = withFlock();
1735
+ return {
1736
+ ...settings,
1737
+ packages: settings.packages.map((pkg) =>
1738
+ pkg.packageId === "provider-ollama-cloud"
1739
+ ? { ...pkg, state: "disabled" as const }
1740
+ : pkg,
1741
+ ),
1742
+ };
1743
+ })();
1744
+ const effective = resolveEffectiveBotModelV1({
1745
+ bot: { packageValues: {} },
1746
+ user: providerOff,
1747
+ packages: flockPackages,
1748
+ });
1749
+ expect(effective.source).toBe("platform");
1750
+ expect(effective.model).toEqual(flockModel);
1751
+ expect(effective.binding?.state).toBe("ready");
1752
+ expect(effective.fallback).toMatchObject({
1753
+ from: "account",
1754
+ model: accountModel,
1755
+ });
1756
+ });
1757
+
1758
+ test("falls back when a Bot's model has lost its Connection", () => {
1759
+ const settings = withFlock();
1760
+ const connectionGone: UserSettingsViewV1 = {
1761
+ ...settings,
1762
+ connections: settings.connections.filter(
1763
+ (connection) => connection.connectionId !== "ollama-work",
1764
+ ),
1765
+ };
1766
+ const effective = resolveEffectiveBotModelV1({
1767
+ bot: { packageValues: { "custom-models": { model: accountModel } } },
1768
+ user: connectionGone,
1769
+ packages: flockPackages,
1770
+ });
1771
+ expect(effective).toMatchObject({ source: "platform", model: flockModel });
1772
+ expect(effective.fallback?.from).toBe("bot");
1773
+ expect(effective.binding?.state).toBe("ready");
1774
+ });
1775
+
1776
+ test("keeps the failure when the platform model cannot stand in either", () => {
1777
+ // Nothing to degrade to: the platform bootstrap is bound to the same
1778
+ // switched-off Package, so the User still gets the repair sentence.
1779
+ const settings = withFlock();
1780
+ const bothOff: UserSettingsViewV1 = {
1781
+ ...settings,
1782
+ packages: settings.packages.map((pkg) =>
1783
+ pkg.packageId === "provider-ollama-cloud"
1784
+ ? { ...pkg, state: "disabled" as const }
1785
+ : pkg,
1786
+ ),
1787
+ platformModel,
1788
+ };
1789
+ const effective = resolveEffectiveBotModelV1({
1790
+ bot: { packageValues: {} },
1791
+ user: bothOff,
1792
+ packages: flockPackages,
1793
+ });
1794
+ expect(effective.source).toBe("account");
1795
+ expect(effective.binding?.state).toBe("unavailable");
1796
+ expect(effective.fallback).toBeUndefined();
1797
+ });
1798
+
1660
1799
  test("reports none only when no Package or platform model supplies one", () => {
1661
1800
  const withoutPlatform = user();
1662
1801
  delete withoutPlatform.platformModel;
package/src/index.ts CHANGED
@@ -604,6 +604,17 @@ export interface EffectiveBotModelV1 {
604
604
  source: "bot" | "account" | "platform" | "none";
605
605
  model?: ModelBindingV1;
606
606
  binding?: ResolvedModelBindingV1;
607
+ /**
608
+ * Set when a Bot or account choice was present but could not bind — its
609
+ * provider Package was switched off, or its Connection is gone — and the
610
+ * platform bootstrap answered in its place. The chosen scope and the reason
611
+ * it failed survive so the client can say the Bot is running on the default.
612
+ */
613
+ fallback?: {
614
+ from: "bot" | "account";
615
+ model: ModelBindingV1;
616
+ failure: string;
617
+ };
607
618
  }
608
619
 
609
620
  /**
@@ -684,23 +695,68 @@ export function resolveEffectiveBotModelV1(input: {
684
695
  };
685
696
  };
686
697
 
698
+ /**
699
+ * A chosen model whose provider Package has been switched off, or whose
700
+ * Connection is gone, must not stop the Bot answering: the platform
701
+ * bootstrap stands in for it. Only a binding failure degrades this way — a
702
+ * scope conflict is the User's own contradiction and still fails closed,
703
+ * because there is no single choice to stand in for.
704
+ */
705
+ const platformStandIn = (
706
+ from: "bot" | "account",
707
+ model: ModelBindingV1,
708
+ binding: ResolvedModelBindingV1,
709
+ ): EffectiveBotModelV1 | undefined => {
710
+ const platform = input.user.platformModel;
711
+ if (!platform) return undefined;
712
+ if (
713
+ platform.connectionId === model.connectionId &&
714
+ platform.providerModelId === model.providerModelId
715
+ ) {
716
+ return undefined;
717
+ }
718
+ const platformBinding = resolveBotModelBindingV1({
719
+ model: platform,
720
+ user: input.user,
721
+ packages: input.packages,
722
+ });
723
+ if (platformBinding.state === "unavailable") return undefined;
724
+ return {
725
+ source: "platform",
726
+ model: structuredClone(platform),
727
+ binding: platformBinding,
728
+ fallback: {
729
+ from,
730
+ model: structuredClone(model),
731
+ failure:
732
+ binding.failure ?? "This Bot's model isn't available right now.",
733
+ },
734
+ };
735
+ };
736
+
687
737
  for (const scope of ["bot", "user"] as const) {
688
738
  const resolved = fromScope(scope);
739
+ const source = scope === "bot" ? "bot" : "account";
689
740
  if (resolved?.conflict) {
690
741
  return {
691
- source: scope === "bot" ? "bot" : "account",
742
+ source,
692
743
  binding: { state: "unavailable", failure: resolved.conflict },
693
744
  };
694
745
  }
695
746
  if (resolved?.model) {
747
+ const binding = resolveBotModelBindingV1({
748
+ model: resolved.model,
749
+ user: input.user,
750
+ packages: input.packages,
751
+ });
752
+ if (binding.state === "unavailable") {
753
+ const standIn = platformStandIn(source, resolved.model, binding);
754
+ if (standIn) return standIn;
755
+ }
696
756
  return {
697
- source: scope === "bot" ? "bot" : "account",
757
+ source,
698
758
  model: structuredClone(resolved.model),
699
- binding: resolveBotModelBindingV1({
700
- model: resolved.model,
701
- user: input.user,
702
- packages: input.packages,
703
- }),
759
+ binding,
704
760
  };
705
761
  }
706
762
  }