1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
//! Inference resolution for the Layer-3 portfolio manager (WI-3/WI-4, #2580/#2581).
//!
//! Why: DOC-36 §3.3 requires every manager LLM call (`/digest`'s narrative pass,
//! `/chat`'s conversational loop) to go through the unified
//! `trusty_common::inference::InferenceAdapter` (epic #2400) — never a bespoke
//! client. This module is the single seam that turns "the daemon's configured
//! provider" into a live `Arc<dyn InferenceAdapter>` for those handlers, and it is
//! the seam the hermetic CI suite (#2584) swaps for a `ScriptedAdapter` /
//! `MockInferenceServer`-backed adapter so no live provider key or network is ever
//! required. Resolution mirrors the commons conventions: a model slug plus the
//! two-stage [`trusty_common::inference::Configurator`]/`provider_for` credential
//! ladder over [`crate::secret_source::credential_store`], with a documented
//! configurable default model.
//! What: [`ManagerInference`] owns either a production credential-backed
//! [`Configurator`] + [`KeyStore`] (built once at provision) or a fixed injected
//! adapter (tests / future hot-swap); [`ManagerInference::resolve`] hands back the
//! `(model, adapter)` pair or a typed [`InferenceUnavailable`] degrade. The model
//! is read from [`MANAGER_MODEL_ENV`] (falling back to [`FALLBACK_MODEL_ENV`] then
//! [`DEFAULT_MANAGER_MODEL`]) — the documented config key for the manager persona.
//! Test: `resolve_reports_no_provider_when_unconfigured`,
//! `resolve_returns_injected_adapter`, `manager_model_env_precedence` in
//! `inference_tests.rs`, plus the HTTP-level digest/chat coverage in
//! `tests/manager_inference.rs`.
use ;
use KeyStore;
use ;
/// Environment key selecting the model slug for the manager's digest/chat calls.
///
/// Why: DOC-36 §3.3 leaves the concrete model configurable; the manager persona
/// wants its own knob (a portfolio digest can prefer a cheaper/faster model than
/// a per-session summariser) without disturbing the fleet-wide
/// [`FALLBACK_MODEL_ENV`]. Pinning a single named key makes the choice
/// documentable (see `docs/reference/environment-variables.md`) and testable.
/// What: the primary env var read by [`resolve_manager_model`].
/// Test: `manager_model_env_precedence`.
pub const MANAGER_MODEL_ENV: &str = "TRUSTY_MANAGER_MODEL";
/// Fleet-wide fallback model key, shared with the activity classifier.
///
/// Why: when an operator has already set `TRUSTY_LLM_MODEL` for the rest of the
/// stack, the manager should honour it rather than force a second variable —
/// mirroring `DaemonState::activity_monitor`'s own `TRUSTY_LLM_MODEL` default.
/// What: the secondary env var read when [`MANAGER_MODEL_ENV`] is unset.
/// Test: `manager_model_env_precedence`.
pub const FALLBACK_MODEL_ENV: &str = "TRUSTY_LLM_MODEL";
/// The default manager model slug when neither env key is set.
///
/// Why: a sensible, low-cost OpenRouter default keeps the manager operable out of
/// the box (matching the `trusty-analyze` deep-pass default), while the two-stage
/// resolver still routes it through whichever provider's credential resolves.
/// What: the fallback slug.
/// Test: `manager_model_env_precedence`.
pub const DEFAULT_MANAGER_MODEL: &str = "openai/gpt-4o-mini";
/// Max tokens requested for a single manager inference call.
///
/// Why: digests and chat replies are short prose; a bounded cap keeps latency and
/// cost predictable for a human-driven surface.
/// What: the `max_tokens` set on every [`trusty_common::inference::ChatRequest`]
/// the manager issues.
/// Test: exercised via `tests/manager_inference.rs`.
pub const MANAGER_MAX_TOKENS: u32 = 900;
/// Sampling temperature for manager inference calls.
///
/// Why: a low temperature keeps digests grounded in the supplied deterministic
/// snapshot rather than inventing portfolio state.
/// What: the `temperature` set on every manager request.
/// Test: side-effect-only; exercised indirectly by the digest happy-path test.
pub const MANAGER_TEMPERATURE: f32 = 0.2;
/// Resolve the configured manager model slug from the environment.
///
/// Why: centralises the documented precedence ([`MANAGER_MODEL_ENV`] >
/// [`FALLBACK_MODEL_ENV`] > [`DEFAULT_MANAGER_MODEL`]) so both provisioning and
/// any diagnostic read agree.
/// What: returns the first non-empty env value in precedence order, else the
/// default slug.
/// Test: `manager_model_env_precedence`.
/// A typed reason the manager could not obtain a live inference adapter.
///
/// Why: DOC-36 §4's degrade bar and #2580's acceptance criteria require a
/// non-panicking, actionable signal when no provider is configured — the handler
/// maps this to a 503 with a deterministic fallback rather than crashing. Keeping
/// it a `thiserror` enum (library convention) lets the handler branch on the
/// cause while surfacing a stable, secret-free message.
/// What: [`Self::NoProvider`] carries a fixed, actionable message; the underlying
/// resolver error is logged (never surfaced) so a credential value can never leak.
/// Test: `resolve_reports_no_provider_when_unconfigured`.
/// How the manager obtains an [`InferenceAdapter`]: credential-resolved or fixed.
///
/// Why: production resolves a provider from the daemon's credential store on
/// demand, while tests (and a future provider hot-swap) inject a concrete adapter.
/// Modelling both as one enum keeps [`ManagerInference::resolve`] a single path.
/// What: [`Self::Credentialed`] holds the registered [`Configurator`] and the
/// resolved [`KeyStore`]; [`Self::Fixed`] holds a ready adapter.
/// Test: `resolve_returns_injected_adapter`, `resolve_reports_no_provider_when_unconfigured`.
/// Inner mutable state of [`ManagerInference`], guarded by a single mutex.
/// The manager's inference seam — resolves a live adapter or a typed degrade.
///
/// Why: [`crate::daemon::manager::ManagerState`] threads exactly one of these so
/// every digest/chat handler shares one provider-resolution policy, and the
/// hermetic suite can swap it for a scripted adapter through the shared `Arc`
/// without a bespoke `DaemonState` constructor. Interior mutability (a plain
/// `Mutex`; reads are rare, human-driven) lets [`Self::set_adapter`] hot-swap the
/// source across all handler clones.
/// What: owns [`Inner`] behind a `Mutex`. [`Self::provision`] builds the
/// production credential-backed variant; [`Self::with_adapter`]/[`Self::set_adapter`]
/// install a fixed adapter; [`Self::resolve`] returns `(model, adapter)` or
/// [`InferenceUnavailable`].
/// Test: `resolve_reports_no_provider_when_unconfigured`, `resolve_returns_injected_adapter`.