Skip to content

feat(config): llm.runMode block — local | cloud | fusion with a fusion cloud-share dial - #161

Open
plombeer31 wants to merge 2 commits into
mainfrom
feat/run-mode-config
Open

feat(config): llm.runMode block — local | cloud | fusion with a fusion cloud-share dial#161
plombeer31 wants to merge 2 commits into
mainfrom
feat/run-mode-config

Conversation

@plombeer31

Copy link
Copy Markdown
Collaborator

Stack: 1 of 3. Foundation only — nothing calls resolveRunMode outside its tests, so this changes no behaviour.

1 llm.runMode config + resolver ← you are here
2 feat/fusion-routing — the routing engine (based on this branch)
3 feat/run-mode-tui — the Run submenu + docs

Why

Switching between the local llama-server model and a cloud provider means walking into Manage → LLM, picking a pane and selecting a model row — several keystrokes deep in a panel you have to leave the chat to reach. The goal of the stack is a submenu on Run with three entries (Local · Cloud · Fusion) that flips models in one gesture, plus a Fusion mode that orchestrates on the cloud model and executes on the local one.

Where the block lives, and why

llm.runMode, a sibling of llm.fallback — not under agent (which is loop budgets) and not top-level (when llm is absent the runtime synthesizes a single local-llama provider, so only local is reachable anyway).

The decisive reason is atomicity: llm has a single writer, and the mode and activeTextProvider have to move in one writeUserConfigFileSync + resetConfigCache() cycle or they can disagree.

The non-contradiction rule

llm.activeTextProvider stays authoritative; runMode.mode is additive:

derived   = kindOf(activeTextProvider) === "llama-server" ? "local" : "cloud"
effective = stored === "fusion" && bothLegsExist && active === cloudId ? "fusion" : derived

Consequences, all deliberate:

  • An operator who switches provider by hand in Manage → LLM simply drops out of fusion on the next read. No reconciliation step, and no state that lies about what is running.
  • Because fusion pins the cloud provider as primary, resolveFallbackChain hoists it to the chain head and appends local at the tail — so fallback-config.ts needs no changes and its locked invariants stay green.

cloudShare is a dial, not a quota

It moves a cutoff on a bounded per-step complexity score (PR 2); it does not promise that N% of steps reach the cloud. 0 behaves exactly like local, 100 exactly like cloud. That is stated in the JSDoc and (in PR 3) in AGENTS.md, so nobody later "fixes" it into a running-counter scheduler — a quota necessarily sends some trivial steps to the cloud and keeps some hard ones local, the opposite of the intent.

Degradation is reported, never silent

Situation Effective Message
cloud/fusion, no cloud provider local names the missing leg, points at Manage → LLM
fusion, no local provider cloud runs cloud-only
fusion, toolTransport pinned fusion warns — a pinned transport sends one leg the wrong wire shape

Config version

USER_CONFIG_VERSION 37 → 38, with no migration code: runMode is an optional sub-key of an already-optional block, so absence is exactly the v37 behaviour. The bump only records the schema change, per the AGENTS.md rule. Pinned by a test that a v37 file upgrades untouched.

Verification

npm run lint and npm run build clean. npm test: 4094 passed, 8 failed — the same 6 files / 8 tests that already fail on main @ 667dae1 (verified by re-running them on a clean checkout of main): stale splash-banner and tui-app fixtures, the localModels.embeddings.url fixture, a dev-machine-specific fs-glob path, and send-message-concurrency. No new failures.

29 new tests across llm-run-mode-config.test.ts, resolve-run-mode.test.ts and run-mode-degradation.test.ts, covering the full degradation matrix and the non-contradiction rule in both directions.

…n cloud-share dial

Adds the config foundation for an operator run mode without changing any
behaviour: nothing calls `resolveRunMode` outside its tests yet.

`llm.runMode` sits beside `llm.fallback` rather than under `agent` (loop
budgets) or at the top level: `llm` has a single writer, and the mode and
`activeTextProvider` must move in one write + cache-reset cycle or they can
disagree.

`activeTextProvider` stays authoritative — `runMode.mode` is additive. The
effective mode is derived from which provider is active, and a stored
`fusion` is honoured only when the cloud leg is the active one. So an
operator who switches provider by hand in Manage → LLM simply drops out of
fusion on the next read: no reconciliation step, and no state that lies
about what is running. It also means fusion pins the cloud provider as the
fallback chain's primary, so `resolveFallbackChain` hoists it to the head
and appends local at the tail with no changes of its own.

`fusion.cloudShare` is documented as a dial, not a quota: it moves the
cutoff on a bounded per-step complexity score rather than promising that
N% of steps reach the cloud.

Degradation is explicit and reported rather than silent — cloud/fusion
without a cloud provider stays local, fusion without a local provider runs
cloud-only, and a pinned `toolTransport` warns without downgrading.

USER_CONFIG_VERSION 37 → 38. No migration code: `runMode` is an optional
sub-key of an already-optional block, so absence is exactly the v37
behaviour; the bump only records the schema change.

Verified: npm run lint and npm run build clean; npm test 4094 passed / 8
failed, the same 6 files / 8 tests that already fail on main @ 667dae1
(stale banner + tui-app fixtures, the `localModels.embeddings.url` fixture,
a dev-machine-specific fs-glob path, and send-message-concurrency).
…mmand

This branch takes USER_CONFIG_VERSION to 38. Hand testing showed what
that does to a machine that also has the installed v0.2.2 release: they
share one ~/.atomic-agent/config.json, so the moment the newer build
wrote v38, every single v0.2.2 command died with

  ConfigValidationError: invalid config: version: unsupported config
  version 38; expected one of 5, 6, ... 37

— models status, config get, the TUI, all of it, with no way out short
of hand-editing the file. Running a release and a build under test side
by side is normal, and the version bump made them mutually exclusive.

Every bump this schema has taken is additive: the parser reads field by
field and writeUserConfigFileSync preserves unknown top-level keys. So a
newer file already parses correctly — the allow-list only ever needed a
floor, not a ceiling. Older-than-supported is still refused, and a
non-integer version is now refused explicitly rather than falling
through the membership test.

ensureUserConfigFileSync gets the other half: it must not rewrite a
newer file back down to this build's shape. Reading it is safe;
overwriting would delete the newer build's keys, and with a shared file
the two builds would take turns destroying each other's config on every
launch.

Knowingly replaces the "rejects unsupported version" test, which pinned
the broken behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant