12 KiB
Provider, Session, and Shared-Contract Boundary Audit
Status: 2026-04-16 audit note
This document audits the current provider, session, and shared-contract seams in the jcode workspace and recommends the next realistic crate moves that improve modularity without creating high-churn dependency cycles.
It is intentionally conservative. The goal is to identify boundaries that are both:
- structurally useful
- low enough churn to be worth turning into workspace crates now
See also:
COMPILE_PERFORMANCE_PLAN.mdREFACTORING.mdSERVER_ARCHITECTURE.mdMULTI_SESSION_CLIENT_ARCHITECTURE.md
Executive summary
The next clean workspace moves are not a full Provider trait extraction and not a full session.rs split.
The best next steps are:
- Add a small
jcode-shared-contractscrate for the serde-only protocol/session overlap types that already act like shared contracts. - After that, add a narrow
jcode-session-contractscrate for session metadata/replay/view structs that are widely reused but do not need the fullSessionruntime. - If we want one more provider-side move before a larger provider refactor, extract the pure provider identity/selection layer into
jcode-provider-coreor a smalljcode-provider-selectioncrate.
The main things to avoid for now:
- extracting
Provider/EventStreaminto a shared crate - extracting all of
protocol.rs - extracting all of
session.rs - moving
provider_catalog.rswholesale into a crate
Those look tempting, but today they would mostly convert existing high-churn coupling into workspace-crate churn.
Current workspace boundary state
Already landed and directionally good:
crates/jcode-provider-metadatacrates/jcode-provider-corecrates/jcode-provider-openroutercrates/jcode-provider-gemini
A useful property of the current extracted crates is that they are still leaf-like support crates.
Current local workspace dependency picture for those crates:
jcode-provider-core: no local workspace depsjcode-provider-metadata: no local workspace depsjcode-provider-openrouter: no local workspace depsjcode-provider-gemini: no local workspace deps
That is the right pattern to preserve. The next crate moves should keep producing small, leaf-ish crates instead of creating new central hubs that everything recompiles through.
Hotspots and coupling observed
Relevant file sizes in the main crate:
src/session.rs: 2730 linessrc/provider/mod.rs: 2283 linessrc/protocol.rs: 1198 linessrc/provider/openrouter.rs: 1132 linessrc/provider/gemini.rs: 1117 linessrc/provider_catalog.rs: 775 linessrc/plan.rs: 17 lines
High-level coupling observed during the audit:
src/provider/mod.rsdirectly referencesauth,logging,bus,message, andusagesrc/session.rsdirectly referencesmessage,protocol,plan,storage, and support modulessrc/protocol.rsdirectly referencesbus,config,message,plan,provider,session, andside_panelsrc/provider_catalog.rsis especially tied toenv,storage, andlogging
That means the biggest blockers are not the already-extracted support crates. They are the remaining mixed runtime/facade modules in the main crate.
Dependency shape
flowchart LR
P[provider/mod.rs] --> AUTH[auth]
P --> MSG[message]
P --> BUS[bus]
P --> USAGE[usage]
S[session.rs] --> MSG
S --> PROTO[protocol.rs]
S --> PLAN[plan.rs]
S --> STORE[storage]
PROTO --> BUS
PROTO --> CFG[config]
PROTO --> MSG
PROTO --> PLAN
PROTO --> PROVIDER_TYPES[provider types]
PROTO --> SESSION_TYPES[session types]
The key architectural smell is that some types that are effectively shared contracts still live inside large mixed-responsibility modules.
Provider boundary audit
What is already in a good state
The existing provider crate moves were well chosen:
jcode-provider-metadataholds stable login/profile catalog datajcode-provider-coreholds route/cost/shared HTTP client/core value typesjcode-provider-openrouterholds OpenRouter-specific catalog/cache/ranking/model-spec supportjcode-provider-geminiholds Gemini Code Assist schema/types/support helpers
These are all relatively pure support surfaces.
What is not a good next move yet
Do not extract Provider / EventStream yet
src/provider/mod.rs is still deeply entangled with:
crate::message::{Message, StreamEvent, ToolDefinition}- auth-driven behavior
- runtime selection/failover
- logging and bus notifications
- provider-specific compaction and transport behavior
Moving the trait now would likely create a new shared crate that still changes whenever runtime/provider behavior changes.
That would improve directory layout, but not boundary quality.
Do not move provider_catalog.rs wholesale yet
src/provider_catalog.rs is not just metadata. It currently mixes:
- catalog/profile values
- env mutation
- auth probing helpers
- config-file lookup
- logging/warnings
That facade is still too runtime-aware to become a clean leaf crate as-is.
Best realistic provider move
Option A: extract provider identity + pure selection
Most realistic provider-side move after the current support crates:
- move the provider identity enum currently represented by
ActiveProvider - move
src/provider/selection.rs - optionally move pure fallback ordering helpers that do not depend on auth/runtime state
Target:
- either a new
crates/jcode-provider-selection - or a small
provider_identity/selectionmodule insidejcode-provider-core
Why this is realistic:
selection.rsis already pure logic- it does not need
Message,EventStream, auth state, or storage - it would shave some policy code out of
src/provider/mod.rs - it creates a stable place for provider-order and provider-name normalization rules
Why this should stay narrow:
- once the code starts touching account failover, auth checks, runtime availability, or logging, it stops being a good crate boundary
Session boundary audit
Why session.rs should not be extracted wholesale yet
src/session.rs is large, but it is not one thing.
It currently mixes:
- persisted session data structures
- runtime session state
- journaling / file persistence helpers
- replay-event persistence
- startup/remote snapshot helpers
- image rendering helpers
A whole-file crate extraction would drag in more coupling than it removes.
Current blockers:
StoredMessagedepends oncrate::message::{ContentBlock, Message, Role, ToolCall}- replay-event types currently depend on
crate::protocol::SwarmMemberStatus - replay-event plan snapshots currently depend on
crate::plan::PlanItem - the session module also owns persistence and storage concerns
So the next move should be a session-contract slice, not a full session crate.
Best realistic session move
Option B: narrow jcode-session-contracts
After shared contracts are extracted first, move the session types that are:
- serde-only
- reused outside
session.rs - not tied to
storageor the fullSessionruntime
Good first candidates:
SessionStatusSessionImproveModeStoredDisplayRoleStoredTokenUsageStoredCompactionStateStoredMemoryInjectionRenderedImageSourceRenderedImageStoredReplayEventandStoredReplayEventKindonce their swarm/plan payloads stop pointing back intoprotocol.rs
What should stay in the main crate for now:
SessionStoredMessage- session journaling/file IO
- session startup/load/save orchestration
- message-to-image rendering functions
Why this is realistic:
- these contract structs already have broad fanout across agent, server, replay, and TUI code
- they are semantically session-level contracts, not session-runtime behavior
- the move becomes much cleaner once shared swarm/protocol payloads are extracted first
Shared-contract boundary audit
This is the highest-leverage next seam.
There are several small, serde-only types that are clearly shared contracts already, but they currently live inside large modules:
PlanIteminsrc/plan.rsTranscriptModeinsrc/protocol.rsCommDeliveryModeinsrc/protocol.rsFeatureToggleinsrc/protocol.rsSessionActivitySnapshotinsrc/protocol.rsSwarmMemberStatusinsrc/protocol.rsAgentInfoinsrc/protocol.rsContextEntryinsrc/protocol.rsSwarmChannelInfoinsrc/protocol.rsAwaitedMemberStatusinsrc/protocol.rsNotificationTypeinsrc/protocol.rs
These are used across server, tool, TUI, replay, and session persistence flows, but they do not need the rest of protocol.rs.
Best overall next move
Option C: add jcode-shared-contracts
Recommended contents for the first pass:
PlanItemTranscriptModeCommDeliveryModeFeatureToggleSessionActivitySnapshot- swarm-related status/info structs:
SwarmMemberStatusAgentInfoContextEntrySwarmChannelInfoAwaitedMemberStatusNotificationType
Why this is the best next move:
- it breaks the
session.rs -> protocol.rs / plan.rsdependency knot at the contract layer - it gives replay/session persistence a clean dependency for swarm and plan snapshots
- it trims
protocol.rswithout trying to extractRequestandServerEventyet - it preserves the current successful pattern of a small, leaf-ish support crate with mostly
serdetypes
Minimal dependency goal:
serde- nothing else, if possible
Recommended sequencing
Phase 1
Create crates/jcode-shared-contracts.
Expected immediate moves:
src/plan.rscontents- the small shared structs/enums listed above from
src/protocol.rs
Keep in main crate for now:
RequestServerEventencode_event/decode_request
Phase 2
Create crates/jcode-session-contracts.
Do this only after Phase 1, so session replay types can point at jcode_shared_contracts::* instead of crate::protocol::* or crate::plan::*.
Phase 3
If a provider-side move is still desired before a larger provider refactor, extract only:
- provider identity enum
- pure selection/fallback ordering helpers
Do not include:
ProvidertraitEventStream- account failover
- auth state inspection
- runtime provider availability
- logging/bus side effects
Moves to explicitly defer
These should be treated as later-stage refactors, not next-step crate moves.
Defer: full protocol.rs crate
Reason:
RequestandServerEventstill pull inmessage,provider,session,side_panel, andbus- extracting the whole file now would create a broad, high-fanout crate instead of a clean contract crate
Defer: full session.rs crate
Reason:
- the file mixes contracts, runtime state, rendering, journaling, and persistence
StoredMessagestill anchors the session layer tomessage.rs
Defer: full provider trait / impl crate split
Reason:
- the trait seam is still mixed with runtime behavior and provider-specific execution policy
- moving it now would likely centralize churn rather than reduce it
Defer: full provider_catalog.rs extraction
Reason:
- the file is still a runtime facade around env/config/auth probing, not just metadata
Why this order avoids dependency-cycle mistakes
The sequence matters:
- extract small shared contracts first
- then extract session contracts that depend on those shared contracts
- only then revisit deeper provider or protocol extraction
That order avoids creating crates that need to point back into the main crate for basic DTOs, which is exactly how high-churn dependency cycles usually start.
Recommended concrete next actions
- Add
crates/jcode-shared-contractswith serde-only types fromplan.rsand the small protocol/session overlap set. - Update
session.rs,protocol.rs, server, tool, replay, and TUI imports to point at that crate. - Re-measure touched-file compile times for:
src/session.rssrc/protocol.rssrc/provider/mod.rs
- If the new seam stays clean, follow with a narrow
jcode-session-contractsextraction. - Revisit provider trait extraction only after message/runtime/provider-execution seams are thinner.