@inetafrica/open-claudia 2.14.8 → 3.0.0

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.
Files changed (157) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +87 -66
  4. package/bin/agent.js +44 -18
  5. package/bin/cli.js +30 -28
  6. package/bin/dream.js +5 -11
  7. package/bin/loopback-client.js +29 -5
  8. package/bin/schedule.js +19 -5
  9. package/bin/tool.js +23 -5
  10. package/bot-agent.js +5 -2350
  11. package/bot.js +105 -7
  12. package/channels/telegram/adapter.js +6 -1
  13. package/channels/voice/adapter.js +1 -0
  14. package/channels/voice/manage.js +43 -5
  15. package/config-dir.js +3 -11
  16. package/core/actions.js +149 -40
  17. package/core/approvals.js +136 -29
  18. package/core/auth-flow.js +103 -19
  19. package/core/config-dir.js +19 -0
  20. package/core/config.js +127 -69
  21. package/core/doctor.js +111 -57
  22. package/core/dream.js +219 -62
  23. package/core/enforcer.js +0 -0
  24. package/core/entities.js +2 -1
  25. package/core/fsutil.js +55 -0
  26. package/core/handlers.js +503 -217
  27. package/core/ideas.js +2 -1
  28. package/core/identity.js +8 -11
  29. package/core/io.js +24 -3
  30. package/core/jobs.js +589 -72
  31. package/core/lessons.js +3 -2
  32. package/core/loopback.js +45 -8
  33. package/core/memory-mutation-queue.js +241 -0
  34. package/core/migration-backup.js +1060 -0
  35. package/core/pack-review.js +295 -88
  36. package/core/packs.js +4 -3
  37. package/core/persona.js +2 -1
  38. package/core/process-tree.js +19 -2
  39. package/core/provider-migration.js +39 -0
  40. package/core/provider-status.js +80 -0
  41. package/core/providers/claude-events.js +117 -0
  42. package/core/providers/claude-hook.js +114 -0
  43. package/core/providers/claude.js +296 -0
  44. package/core/providers/codex-events.js +121 -0
  45. package/core/providers/codex-hook.js +280 -0
  46. package/core/providers/codex.js +286 -0
  47. package/core/providers/contract.js +153 -0
  48. package/core/providers/env.js +148 -0
  49. package/core/providers/events.js +170 -0
  50. package/core/providers/hook-command.js +22 -0
  51. package/core/providers/index.js +240 -0
  52. package/core/providers/utility-policy.js +269 -0
  53. package/core/recall/classic.js +2 -2
  54. package/core/recall/discoverer.js +71 -22
  55. package/core/recall/metrics.js +27 -1
  56. package/core/recall/tuning.js +4 -1
  57. package/core/recall/warm-walker.js +151 -108
  58. package/core/recall-filter.js +86 -61
  59. package/core/router.js +79 -7
  60. package/core/run-context.js +185 -0
  61. package/core/runner.js +1415 -1282
  62. package/core/scheduler.js +515 -210
  63. package/core/side-chat.js +346 -0
  64. package/core/state.js +1096 -95
  65. package/core/subagent.js +72 -98
  66. package/core/system-prompt.js +462 -279
  67. package/core/tool-guard.js +73 -39
  68. package/core/tools.js +22 -5
  69. package/core/transcripts.js +30 -1
  70. package/core/usage-log.js +19 -4
  71. package/core/utility-agent.js +654 -0
  72. package/core/web-auth.js +29 -13
  73. package/core/web-sessions.js +78 -0
  74. package/docs/CHANNEL_DESIGN.md +181 -0
  75. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  76. package/docs/PROVIDER_MIGRATION.md +67 -0
  77. package/health.js +50 -39
  78. package/package.json +51 -4
  79. package/setup.js +198 -38
  80. package/soul.md +39 -0
  81. package/test-ability-extraction.js +5 -0
  82. package/test-approval-async.js +63 -4
  83. package/test-cursor-removal.js +334 -0
  84. package/test-delivery-contract.js +88 -0
  85. package/test-enforcer.js +70 -27
  86. package/test-fixtures/current-provider-parity.json +132 -0
  87. package/test-fixtures/fake-agent-cli.js +477 -0
  88. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  89. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  90. package/test-fixtures/migrations/claude-only/state.json +5 -0
  91. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  92. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  93. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  94. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  95. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  96. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  97. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  98. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  99. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  100. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  101. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  102. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  103. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  104. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  105. package/test-fixtures/migrations/missing-project/state.json +4 -0
  106. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  107. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  108. package/test-fixtures/migrations/multi-user/state.json +6 -0
  109. package/test-fixtures/provider-event-samples.json +17 -0
  110. package/test-learning-e2e.js +5 -0
  111. package/test-memory-mutation-queue.js +191 -0
  112. package/test-provider-boot-matrix.js +399 -0
  113. package/test-provider-bootstrap.js +158 -0
  114. package/test-provider-capabilities.js +232 -0
  115. package/test-provider-claude.js +206 -0
  116. package/test-provider-codex.js +228 -0
  117. package/test-provider-compaction.js +371 -0
  118. package/test-provider-core-prompt.js +264 -0
  119. package/test-provider-coupling-audit.js +90 -0
  120. package/test-provider-dream.js +312 -0
  121. package/test-provider-enforcer.js +252 -0
  122. package/test-provider-env-isolation.js +150 -0
  123. package/test-provider-events.js +141 -0
  124. package/test-provider-fixture.js +332 -0
  125. package/test-provider-gateway.js +508 -0
  126. package/test-provider-language.js +89 -0
  127. package/test-provider-migration-backup.js +424 -0
  128. package/test-provider-pack-review.js +436 -0
  129. package/test-provider-parity-e2e.js +537 -0
  130. package/test-provider-prompt-parity.js +89 -0
  131. package/test-provider-recall.js +271 -0
  132. package/test-provider-registry.js +251 -0
  133. package/test-provider-resume-parity.js +41 -0
  134. package/test-provider-run-lifecycle.js +349 -0
  135. package/test-provider-runner.js +271 -0
  136. package/test-provider-scheduler.js +689 -0
  137. package/test-provider-session-commands.js +185 -0
  138. package/test-provider-session-history.js +205 -0
  139. package/test-provider-side-chat.js +337 -0
  140. package/test-provider-state-migration.js +316 -0
  141. package/test-provider-stream-decoder.js +69 -0
  142. package/test-provider-subagent.js +228 -0
  143. package/test-provider-tool-hooks.js +360 -0
  144. package/test-recall-discoverer.js +1 -0
  145. package/test-recall-engine.js +3 -3
  146. package/test-recall-evolution.js +18 -0
  147. package/test-run-lock.js +63 -0
  148. package/test-runner-watchdog-static.js +16 -8
  149. package/test-single-runtime.js +35 -0
  150. package/test-telegram-poll-recovery.js +56 -0
  151. package/test-tool-guard.js +56 -0
  152. package/test-tools.js +19 -0
  153. package/test-unified-identity.js +3 -1
  154. package/test-usage-accounting.js +92 -20
  155. package/test-utility-provider-policy.js +486 -0
  156. package/test-web-sessions.js +74 -0
  157. package/web.js +159 -45
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+
3
+ const { validateProvider, validateTypedStatus } = require("./contract");
4
+
5
+ const DEFAULT_PROVIDER_IDS = Object.freeze(["claude", "codex"]);
6
+ const DEFAULT_STATUS_TTL_MS = 30_000;
7
+
8
+ class ProviderRegistryError extends Error {
9
+ constructor(code, message, details = {}) {
10
+ super(message);
11
+ this.name = "ProviderRegistryError";
12
+ this.code = code;
13
+ this.details = details;
14
+ }
15
+ }
16
+
17
+ class ProviderRegistry {
18
+ constructor(options = {}) {
19
+ this.allowedProviderIds = new Set(options.allowedProviderIds || DEFAULT_PROVIDER_IDS);
20
+ this.defaultProvider = options.defaultProvider == null
21
+ ? null
22
+ : String(options.defaultProvider).trim().toLowerCase();
23
+ this.providers = new Map();
24
+ this.statusTtlMs = Number.isFinite(options.statusTtlMs) ? options.statusTtlMs : DEFAULT_STATUS_TTL_MS;
25
+ this.now = typeof options.now === "function" ? options.now : Date.now;
26
+ this.statusCache = new Map();
27
+ }
28
+
29
+ registerProvider(provider) {
30
+ validateProvider(provider);
31
+ if (!this.allowedProviderIds.has(provider.id)) {
32
+ throw new ProviderRegistryError("UNKNOWN_PROVIDER", `Unknown provider: ${provider.id}`, { providerId: provider.id });
33
+ }
34
+ if (this.providers.has(provider.id)) {
35
+ throw new ProviderRegistryError("DUPLICATE_PROVIDER", `Provider already registered: ${provider.id}`, { providerId: provider.id });
36
+ }
37
+ this.providers.set(provider.id, provider);
38
+ return provider;
39
+ }
40
+
41
+ getProvider(providerId) {
42
+ if (!this.allowedProviderIds.has(providerId) || !this.providers.has(providerId)) {
43
+ throw new ProviderRegistryError("UNKNOWN_PROVIDER", `Unknown provider: ${providerId}`, { providerId });
44
+ }
45
+ return this.providers.get(providerId);
46
+ }
47
+
48
+ listProviders() {
49
+ return [...this.providers.values()];
50
+ }
51
+
52
+ providerStatus(providerId, options = {}) {
53
+ const provider = this.getProvider(providerId);
54
+ const wantAuth = options.probeAuth !== false;
55
+
56
+ // Availability is a cheap executable lookup and always runs fresh, so a
57
+ // provider that disappears (or moves) mid-session is detected immediately.
58
+ let available = false;
59
+ let executable = null;
60
+ let availabilityReason = null;
61
+ try {
62
+ available = provider.isAvailable() === true;
63
+ if (available) executable = provider.executable() || null;
64
+ if (available && !executable) {
65
+ available = false;
66
+ availabilityReason = "Provider reported available without an executable";
67
+ }
68
+ } catch (error) {
69
+ availabilityReason = error.message;
70
+ }
71
+
72
+ if (!available) {
73
+ this.statusCache.delete(provider.id);
74
+ return {
75
+ id: provider.id,
76
+ label: provider.label,
77
+ availability: { state: "missing", executable: null, reason: availabilityReason || "Provider executable is unavailable" },
78
+ compatibility: { state: "not_checked", reason: "Provider executable is unavailable" },
79
+ auth: { state: "not_checked", reason: "Provider executable is unavailable" },
80
+ };
81
+ }
82
+
83
+ // The expensive CLI probes (version/help spawns, auth status) are served
84
+ // from a short TTL cache keyed to the same executable path. Only a fully
85
+ // healthy, auth-probed status is cached: negative results always re-probe
86
+ // so an install or login recovers immediately, and a no-auth probe never
87
+ // masks an auth regression for auth-requiring callers.
88
+ if (options.refresh !== true && this.statusTtlMs > 0) {
89
+ const cached = this.statusCache.get(provider.id);
90
+ if (cached
91
+ && (this.now() - cached.at) < this.statusTtlMs
92
+ && cached.status.availability.executable === executable) {
93
+ const status = structuredClone(cached.status);
94
+ if (!wantAuth) status.auth = { state: "not_checked", reason: "Authentication was not requested" };
95
+ return status;
96
+ }
97
+ }
98
+
99
+ const status = this.#probeStatus(provider, options, executable);
100
+ if (this.statusTtlMs > 0
101
+ && wantAuth
102
+ && status.compatibility.state === "compatible"
103
+ && status.auth.state === "authenticated") {
104
+ this.statusCache.set(provider.id, { at: this.now(), status: structuredClone(status) });
105
+ }
106
+ return status;
107
+ }
108
+
109
+ clearStatusCache(providerId = null) {
110
+ if (providerId === null) this.statusCache.clear();
111
+ else this.statusCache.delete(providerId);
112
+ }
113
+
114
+ #probeStatus(provider, options, executable) {
115
+ let compatibility;
116
+ try {
117
+ compatibility = validateTypedStatus(provider.compatibilityStatus(), "compatibility");
118
+ } catch (error) {
119
+ compatibility = { state: "unknown", reason: "Compatibility probe failed" };
120
+ }
121
+ const auth = compatibility.state === "incompatible" || options.probeAuth === false
122
+ ? { state: "not_checked", reason: compatibility.state === "incompatible" ? "Provider is incompatible" : "Authentication was not requested" }
123
+ : (() => {
124
+ try { return validateTypedStatus(provider.authStatus(), "auth"); }
125
+ catch (error) { return { state: "unknown", reason: "Authentication probe failed" }; }
126
+ })();
127
+ return {
128
+ id: provider.id,
129
+ label: provider.label,
130
+ availability: { state: "available", executable, reason: null },
131
+ compatibility,
132
+ auth,
133
+ };
134
+ }
135
+
136
+ availableProviders() {
137
+ return this.listProviders().filter((provider) => {
138
+ const status = this.providerStatus(provider.id, { probeAuth: false });
139
+ return status.availability.state === "available" && status.compatibility.state !== "incompatible";
140
+ });
141
+ }
142
+
143
+ selectDefaultProvider(preferredId = this.defaultProvider) {
144
+ const preferred = preferredId == null || String(preferredId).trim() === ""
145
+ ? null
146
+ : String(preferredId).trim().toLowerCase();
147
+ if (preferred) {
148
+ if (!this.allowedProviderIds.has(preferred) || !this.providers.has(preferred)) {
149
+ return {
150
+ providerId: null,
151
+ source: "explicit",
152
+ reason: `Unknown default provider: ${preferred}`,
153
+ };
154
+ }
155
+ const status = this.providerStatus(preferred, { probeAuth: false });
156
+ if (status.availability.state !== "available") {
157
+ return { providerId: null, source: "explicit", reason: `${status.label} is unavailable` };
158
+ }
159
+ if (status.compatibility.state !== "compatible") {
160
+ return {
161
+ providerId: null,
162
+ source: "explicit",
163
+ reason: status.compatibility.reason || `${status.label} compatibility could not be confirmed`,
164
+ };
165
+ }
166
+ return { providerId: preferred, source: "explicit", reason: null };
167
+ }
168
+
169
+ for (const provider of this.listProviders()) {
170
+ const status = this.providerStatus(provider.id, { probeAuth: false });
171
+ if (status.availability.state === "available" && status.compatibility.state === "compatible") {
172
+ return { providerId: provider.id, source: "detected", reason: null };
173
+ }
174
+ }
175
+ return {
176
+ providerId: null,
177
+ source: "detected",
178
+ reason: "No compatible provider is configured or installed",
179
+ };
180
+ }
181
+
182
+ resolveProviderForRun(providerId = null) {
183
+ if (providerId !== null && providerId !== undefined) {
184
+ const provider = this.getProvider(providerId);
185
+ return this.#assertRunnable(provider);
186
+ }
187
+
188
+ if (this.defaultProvider) {
189
+ const provider = this.getProvider(this.defaultProvider);
190
+ return this.#assertRunnable(provider);
191
+ }
192
+
193
+ const selection = this.selectDefaultProvider();
194
+ if (!selection.providerId) {
195
+ throw new ProviderRegistryError(
196
+ "NO_AVAILABLE_PROVIDER",
197
+ "No compatible provider is available. Configure Claude Code or OpenAI Codex before starting a model turn.",
198
+ );
199
+ }
200
+ // Foreground selection is deterministic and never silently changes the
201
+ // provider because authentication failed. Explicit fallback belongs to the
202
+ // utility-provider policy, where the caller opts into it and records it.
203
+ return this.#assertRunnable(this.getProvider(selection.providerId));
204
+ }
205
+
206
+ #assertRunnable(provider) {
207
+ const status = this.providerStatus(provider.id);
208
+ if (status.availability.state !== "available") {
209
+ throw new ProviderRegistryError("PROVIDER_UNAVAILABLE", `${provider.label} is unavailable`, { providerId: provider.id, status });
210
+ }
211
+ if (status.compatibility.state === "incompatible") {
212
+ throw new ProviderRegistryError("PROVIDER_INCOMPATIBLE", `${provider.label} is incompatible: ${status.compatibility.reason || "unsupported version"}`, { providerId: provider.id, status });
213
+ }
214
+ if (status.auth.state === "unauthenticated") {
215
+ throw new ProviderRegistryError("PROVIDER_UNAUTHENTICATED", `${provider.label} is not authenticated: ${status.auth.reason || "login required"}`, { providerId: provider.id, status });
216
+ }
217
+ return provider;
218
+ }
219
+ }
220
+
221
+ const registry = new ProviderRegistry({
222
+ defaultProvider: require("../config").DEFAULT_PROVIDER,
223
+ });
224
+ registry.registerProvider(require("./claude").claudeProvider);
225
+ registry.registerProvider(require("./codex").codexProvider);
226
+
227
+ module.exports = {
228
+ DEFAULT_PROVIDER_IDS,
229
+ ProviderRegistry,
230
+ ProviderRegistryError,
231
+ availableProviders: registry.availableProviders.bind(registry),
232
+ clearProviderStatusCache: registry.clearStatusCache.bind(registry),
233
+ getProvider: registry.getProvider.bind(registry),
234
+ listProviders: registry.listProviders.bind(registry),
235
+ providerStatus: registry.providerStatus.bind(registry),
236
+ registerProvider: registry.registerProvider.bind(registry),
237
+ resolveProviderForRun: registry.resolveProviderForRun.bind(registry),
238
+ selectDefaultProvider: registry.selectDefaultProvider.bind(registry),
239
+ registry,
240
+ };
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+
3
+ const config = require("../config");
4
+
5
+ const PROVIDER_IDS = Object.freeze(["claude", "codex"]);
6
+ const PROVIDER_ID_SET = new Set(PROVIDER_IDS);
7
+ const PURPOSES = Object.freeze(["subagent", "recall", "review", "dream", "introspection", "enforcer"]);
8
+ const PURPOSE_SET = new Set(PURPOSES);
9
+ const TIERS = Object.freeze(["low", "medium", "high"]);
10
+ const TIER_SET = new Set(TIERS);
11
+ const PURPOSE_PROVIDER_ENV = Object.freeze({
12
+ subagent: "SUBAGENT_PROVIDER",
13
+ recall: "RECALL_PROVIDER",
14
+ review: "MEMORY_PROVIDER",
15
+ dream: "DREAM_PROVIDER",
16
+ introspection: "DREAM_PROVIDER",
17
+ enforcer: "ENFORCER_PROVIDER",
18
+ });
19
+ const PURPOSE_MODEL_PREFIX = Object.freeze({
20
+ subagent: "SUBAGENT_MODEL",
21
+ recall: "RECALL_MODEL",
22
+ review: "PACK_REVIEW_MODEL",
23
+ dream: "DREAM_MODEL",
24
+ introspection: "DREAM_MODEL",
25
+ enforcer: "ENFORCER_MODEL",
26
+ });
27
+ const LEGACY_CLAUDE_MODEL_ENV = Object.freeze({
28
+ review: "PACK_REVIEW_MODEL",
29
+ dream: "DREAM_MODEL",
30
+ introspection: "DREAM_MODEL",
31
+ enforcer: "ENFORCER_MODEL",
32
+ });
33
+
34
+ class UtilityPolicyError extends Error {
35
+ constructor(code, message, details = {}) {
36
+ super(message);
37
+ this.name = "UtilityPolicyError";
38
+ this.code = code;
39
+ this.details = details;
40
+ }
41
+ }
42
+
43
+ function normalizeProvider(value, { allowActive = false, allowEmpty = false } = {}) {
44
+ const provider = String(value == null ? "" : value).trim().toLowerCase();
45
+ if (!provider && allowEmpty) return null;
46
+ if (PROVIDER_ID_SET.has(provider) || (allowActive && provider === "active")) return provider;
47
+ throw new UtilityPolicyError(
48
+ "INVALID_UTILITY_PROVIDER",
49
+ `Utility provider must be ${allowActive ? "active, " : ""}claude, or codex`,
50
+ { provider: provider || null },
51
+ );
52
+ }
53
+
54
+ function normalizePurpose(value) {
55
+ const purpose = String(value || "").trim().toLowerCase();
56
+ if (!PURPOSE_SET.has(purpose)) {
57
+ throw new UtilityPolicyError("INVALID_UTILITY_PURPOSE", `Unknown utility purpose: ${purpose || "(empty)"}`, { purpose });
58
+ }
59
+ return purpose;
60
+ }
61
+
62
+ function normalizeTier(value) {
63
+ const tier = String(value || "medium").trim().toLowerCase();
64
+ if (!TIER_SET.has(tier)) {
65
+ throw new UtilityPolicyError("INVALID_UTILITY_TIER", `Utility tier must be low, medium, or high`, { tier });
66
+ }
67
+ return tier;
68
+ }
69
+
70
+ function parseProviderFallbacks(value) {
71
+ const values = Array.isArray(value) ? value : String(value || "").split(",");
72
+ const result = [];
73
+ for (const raw of values) {
74
+ const provider = normalizeProvider(raw, { allowEmpty: true });
75
+ if (provider && !result.includes(provider)) result.push(provider);
76
+ }
77
+ return result;
78
+ }
79
+
80
+ function defaultActiveProvider(env = process.env) {
81
+ const inherited = String(env?.OC_PROVIDER || process.env.OC_PROVIDER || "").trim().toLowerCase();
82
+ if (PROVIDER_ID_SET.has(inherited)) return inherited;
83
+ try {
84
+ const { currentCanonicalUserId } = require("../context");
85
+ const canonicalUserId = currentCanonicalUserId();
86
+ if (!canonicalUserId) return null;
87
+ const state = require("../state");
88
+ return state.getActiveProvider(state.getUserState(canonicalUserId));
89
+ } catch (_) {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function registryProviders(registry) {
95
+ try {
96
+ const listed = registry.listProviders();
97
+ return listed.map((provider) => provider.id).filter((id) => PROVIDER_ID_SET.has(id));
98
+ } catch (_) {
99
+ return [...PROVIDER_IDS];
100
+ }
101
+ }
102
+
103
+ function runnableStatus(registry, providerId) {
104
+ let status;
105
+ try { status = registry.providerStatus(providerId); }
106
+ catch (error) {
107
+ return { runnable: false, code: error.code || "PROVIDER_UNAVAILABLE", reason: error.message || "provider status failed", status: null };
108
+ }
109
+ if (status?.availability?.state !== "available") {
110
+ return { runnable: false, code: "PROVIDER_UNAVAILABLE", reason: status?.availability?.reason || "provider executable is unavailable", status };
111
+ }
112
+ if (status?.compatibility?.state === "incompatible") {
113
+ return { runnable: false, code: "PROVIDER_INCOMPATIBLE", reason: status.compatibility.reason || "provider is incompatible", status };
114
+ }
115
+ if (status?.auth?.state === "unauthenticated") {
116
+ return { runnable: false, code: "PROVIDER_UNAUTHENTICATED", reason: status.auth.reason || "provider is not authenticated", status };
117
+ }
118
+ return { runnable: true, code: null, reason: null, status };
119
+ }
120
+
121
+ function envValue(env, key) {
122
+ return String(env?.[key] == null ? "" : env[key]).trim();
123
+ }
124
+
125
+ function exactModelOverride(env, purpose, providerId) {
126
+ const prefix = PURPOSE_MODEL_PREFIX[purpose];
127
+ if (prefix) {
128
+ const specific = envValue(env, `${prefix}_${providerId.toUpperCase()}`);
129
+ if (specific) return specific;
130
+ }
131
+ if (providerId === "claude") {
132
+ const legacyKey = LEGACY_CLAUDE_MODEL_ENV[purpose];
133
+ const legacy = legacyKey ? envValue(env, legacyKey) : "";
134
+ if (legacy) return legacy;
135
+ }
136
+ return null;
137
+ }
138
+
139
+ function modelFor(provider, request, env) {
140
+ const { purpose, tier, model } = request;
141
+ if (typeof model === "string" && model.trim()) return model.trim();
142
+ const customPrefix = String(request.modelEnvPrefix || "").trim();
143
+ if (customPrefix) {
144
+ if (!/^[A-Z][A-Z0-9_]*$/.test(customPrefix)) {
145
+ throw new UtilityPolicyError("INVALID_UTILITY_MODEL_OVERRIDE", "modelEnvPrefix must be an uppercase environment-key prefix");
146
+ }
147
+ const specific = envValue(env, `${customPrefix}_${provider.id.toUpperCase()}`);
148
+ if (specific) return specific;
149
+ }
150
+ const legacyKey = String(request.legacyClaudeModelEnv || "").trim();
151
+ if (provider.id === "claude" && legacyKey) {
152
+ if (!/^[A-Z][A-Z0-9_]*$/.test(legacyKey)) {
153
+ throw new UtilityPolicyError("INVALID_UTILITY_MODEL_OVERRIDE", "legacyClaudeModelEnv must be an uppercase environment key");
154
+ }
155
+ const legacy = envValue(env, legacyKey);
156
+ if (legacy) return legacy;
157
+ }
158
+ const exact = exactModelOverride(env, purpose, provider.id);
159
+ if (exact) return exact;
160
+ if (typeof provider.tierModel === "function") return provider.tierModel(tier);
161
+ return provider.defaultModel() || null;
162
+ }
163
+
164
+ function createUtilityPolicy(options = {}) {
165
+ const registry = options.registry || require("./index");
166
+ const env = options.env || config.config;
167
+ const activeProvider = typeof options.activeProvider === "function" ? options.activeProvider : () => defaultActiveProvider(env);
168
+
169
+ function candidateIds(request) {
170
+ const purpose = normalizePurpose(request.purpose);
171
+ const explicit = request.provider == null ? null : normalizeProvider(request.provider, { allowActive: true });
172
+ const purposeSetting = envValue(env, PURPOSE_PROVIDER_ENV[purpose]);
173
+ const configured = explicit || (purposeSetting ? normalizeProvider(purposeSetting, { allowActive: true }) : null)
174
+ || normalizeProvider(envValue(env, "UTILITY_PROVIDER") || "active", { allowActive: true });
175
+
176
+ let preferred = configured;
177
+ if (preferred === "active") {
178
+ const capturedActive = request.activeProvider == null
179
+ ? null
180
+ : normalizeProvider(request.activeProvider, { allowEmpty: true });
181
+ const active = capturedActive || activeProvider();
182
+ if (active) preferred = normalizeProvider(active);
183
+ else {
184
+ const globalDefault = envValue(env, "DEFAULT_PROVIDER");
185
+ preferred = globalDefault
186
+ ? normalizeProvider(globalDefault)
187
+ : (registryProviders(registry).find((providerId) => runnableStatus(registry, providerId).runnable) || null);
188
+ }
189
+ }
190
+ if (!preferred) {
191
+ throw new UtilityPolicyError("NO_AVAILABLE_UTILITY_PROVIDER", "No default utility provider is configured", { purpose });
192
+ }
193
+ const fallbacks = parseProviderFallbacks(
194
+ request.fallbacks !== undefined ? request.fallbacks : env?.PROVIDER_FALLBACKS,
195
+ );
196
+ return {
197
+ purpose,
198
+ preferred,
199
+ ids: [preferred, ...fallbacks.filter((id) => id !== preferred)],
200
+ };
201
+ }
202
+
203
+ function candidates(request = {}) {
204
+ const purpose = normalizePurpose(request.purpose);
205
+ const tier = normalizeTier(request.tier);
206
+ const selection = candidateIds({ ...request, purpose });
207
+ const attempts = [];
208
+ const runnable = [];
209
+ for (const providerId of selection.ids) {
210
+ const probe = runnableStatus(registry, providerId);
211
+ attempts.push({ providerId, code: probe.code, reason: probe.reason });
212
+ if (!probe.runnable) continue;
213
+ let provider;
214
+ try { provider = registry.getProvider(providerId); }
215
+ catch (error) {
216
+ attempts[attempts.length - 1] = { providerId, code: error.code || "UNKNOWN_PROVIDER", reason: error.message };
217
+ continue;
218
+ }
219
+ runnable.push({
220
+ purpose,
221
+ tier,
222
+ provider,
223
+ providerId,
224
+ preferredProviderId: selection.preferred,
225
+ fallbackUsed: providerId !== selection.preferred,
226
+ model: modelFor(provider, { ...request, purpose, tier }, env),
227
+ status: probe.status,
228
+ attempts: attempts.map((attempt) => ({ ...attempt })),
229
+ });
230
+ }
231
+ if (!runnable.length) {
232
+ throw new UtilityPolicyError(
233
+ "NO_AVAILABLE_UTILITY_PROVIDER",
234
+ `No configured provider can run the ${purpose} utility task`,
235
+ { purpose, preferredProviderId: selection.preferred, attempts },
236
+ );
237
+ }
238
+ return runnable;
239
+ }
240
+
241
+ return {
242
+ candidates,
243
+ resolve: (request) => candidates(request)[0],
244
+ };
245
+ }
246
+
247
+ let defaultPolicy = null;
248
+ function resolveUtilityProvider(request) {
249
+ if (!defaultPolicy) defaultPolicy = createUtilityPolicy();
250
+ return defaultPolicy.resolve(request);
251
+ }
252
+
253
+ module.exports = {
254
+ LEGACY_CLAUDE_MODEL_ENV,
255
+ PROVIDER_IDS,
256
+ PURPOSES,
257
+ PURPOSE_MODEL_PREFIX,
258
+ PURPOSE_PROVIDER_ENV,
259
+ TIERS,
260
+ UtilityPolicyError,
261
+ createUtilityPolicy,
262
+ defaultActiveProvider,
263
+ exactModelOverride,
264
+ normalizePurpose,
265
+ normalizeTier,
266
+ parseProviderFallbacks,
267
+ resolveUtilityProvider,
268
+ runnableStatus,
269
+ };
@@ -11,7 +11,7 @@
11
11
  // are injected here so module-local injection caches / lastInjected keep working.
12
12
 
13
13
  async function run(ctx) {
14
- const { userText, fullContext, packLimit, budget, helpers } = ctx;
14
+ const { userText, fullContext, packLimit, budget, helpers, provider } = ctx;
15
15
  const {
16
16
  packsLib, entitiesLib, mergeMatches, filterMatches, logRecall,
17
17
  buildPackBlock, buildEntityBlock,
@@ -36,7 +36,7 @@ async function run(ctx) {
36
36
 
37
37
  const candPacks = packMatches;
38
38
  const candEntities = entityMatches;
39
- ({ packMatches, entityMatches } = await filterMatches(userText, fullContext, packMatches, entityMatches));
39
+ ({ packMatches, entityMatches } = await filterMatches(userText, fullContext, packMatches, entityMatches, { provider }));
40
40
  packMatches = packMatches.slice(0, packLimit);
41
41
  entityMatches = entityMatches.slice(0, 4);
42
42
  logRecall(userText, candPacks, candEntities, packMatches, entityMatches);