dsh-auto-model-routing 0.4.0 → 0.4.2

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.
@@ -5,6 +5,7 @@ import { choosePolicyRoute, mappingVersion, policyEffort, modelFamily, policyPro
5
5
  import { userInput } from './input.js';
6
6
  import { buildState, previousTurn } from './context.js';
7
7
  import { isRecord } from './settings-validation.js';
8
+ import { autoSupportedModel, currentMainSelection, mainSupported } from './support.js';
8
9
  export function policyHost(ctx, deps) {
9
10
  const delegation = new AsyncLocalStorage();
10
11
  const rootOf = (agent) => {
@@ -27,6 +28,13 @@ export function policyHost(ctx, deps) {
27
28
  const decidedAt = new Map();
28
29
  const applied = new Map();
29
30
  const latest = new Map();
31
+ /** Fall back to MANUAL when the main model left AUTO's Claude/GPT scope. */
32
+ const lockUnsupportedMain = (agent) => {
33
+ deps.state.setMode(agent.id, 'MANUAL');
34
+ mainStatus.delete(agent.id);
35
+ latest.delete(agent.id);
36
+ applied.delete(agent.id);
37
+ };
30
38
  ctx.on('tools/execute', async (exec, next) => {
31
39
  if (!['subagent', 'subagent_fork', 'spawn_teammate'].includes(exec.name) || !exec.agent)
32
40
  return next();
@@ -80,6 +88,11 @@ export function policyHost(ctx, deps) {
80
88
  main.set(agent, { ...old, turn: payload.turn });
81
89
  return decision;
82
90
  }
91
+ // The main model left AUTO's Claude/GPT scope: judge nothing, rewrite nothing.
92
+ if (!mainSupported(currentMainSelection(ctx, agent))) {
93
+ lockUnsupportedMain(agent);
94
+ return decision;
95
+ }
83
96
  applied.delete(agent.id);
84
97
  mainStatus.delete(agent.id);
85
98
  const startedEpoch = epoch(agent.id);
@@ -190,17 +203,43 @@ export function policyHost(ctx, deps) {
190
203
  const runtime = main.get(agent);
191
204
  if (!runtime?.active || runtime.turn !== payload.turn || runtime.epoch !== epoch(agent.id) || !enabled(agent.id))
192
205
  return config;
206
+ // Authoritative gate: the request's actual model decides, whatever the picker showed earlier.
207
+ if (!autoSupportedModel(config.model)) {
208
+ lockUnsupportedMain(agent);
209
+ return config;
210
+ }
211
+ const unsupported = (effort = '模型不支持调整') => {
212
+ const old = mainStatus.get(agent.id);
213
+ mainStatus.set(agent.id, { ...old, policyVersion, depth: runtime.depth, reason: 'no-effort-support', ...(old?.decision ? { decision: { ...old.decision, status: 'unsupported', model: config.model, provider: config.provider, effort } } : {}) });
214
+ };
193
215
  const info = await ctx.llm.resolveModelInfo(config.provider, config.model, payload.signal);
194
216
  if (!enabled(agent.id) || runtime.epoch !== epoch(agent.id) || payload.signal.aborted)
195
217
  return config;
196
218
  const efforts = (info.reasoning?.efforts ?? []).map(e => String(e.id));
197
219
  if (!efforts.length) {
198
- const old = mainStatus.get(agent.id);
199
- mainStatus.set(agent.id, { ...old, policyVersion, depth: runtime.depth, reason: 'no-effort-support', ...(old?.decision ? { decision: { ...old.decision, status: 'unsupported', model: config.model, provider: config.provider, effort: '模型不支持调整' } } : {}) });
220
+ unsupported();
221
+ return config;
222
+ }
223
+ let reasoningEffort;
224
+ try {
225
+ reasoningEffort = policyEffort(config.model, runtime.depth, efforts, deps.settings().effortCap, info.reasoning?.defaultEffort ? String(info.reasoning.defaultEffort) : undefined);
226
+ }
227
+ catch (error) {
228
+ // A supported model whose directory cannot produce an automatic effort must not fail the turn.
229
+ ctx.logger.warn(`auto-model-routing: could not map the main reasoning effort: ${String(error)}`);
230
+ unsupported();
200
231
  return config;
201
232
  }
202
- const reasoningEffort = policyEffort(config.model, runtime.depth, efforts, deps.settings().effortCap, info.reasoning?.defaultEffort ? String(info.reasoning.defaultEffort) : undefined);
203
233
  mainStatus.set(agent.id, { ...mainStatus.get(agent.id), policyVersion, depth: runtime.depth, effort: reasoningEffort, reason: 'jev', ...(mainStatus.get(agent.id)?.decision ? { decision: { ...mainStatus.get(agent.id).decision, effort: reasoningEffort, model: config.model, provider: config.provider } } : {}) });
234
+ if (reasoningEffort !== config.reasoningEffort) {
235
+ try {
236
+ await deps.selectModel({ sessionId: agent.id, provider: config.provider, model: config.model, reasoningEffort });
237
+ }
238
+ catch (error) {
239
+ ctx.logger.warn(`auto-model-routing: could not sync the model selection: ${String(error)}`);
240
+ return config;
241
+ }
242
+ }
204
243
  return { ...config, reasoningEffort };
205
244
  }
206
245
  const record = deps.store.get(agent.id);
@@ -217,8 +256,29 @@ export function policyHost(ctx, deps) {
217
256
  validatePolicyRoute(await deps.catalog(), record.route.provider, record.route.model, record.route.reasoningEffort, deps.settings().effortCap);
218
257
  return { ...config, ...record.route };
219
258
  }, { prepend: true });
259
+ const confirmMainRequest = (session, requestSeq, turn) => {
260
+ const status = mainStatus.get(session.id);
261
+ const actual = session.requestHeader()?.config;
262
+ if (!enabled(session.id) || !status?.decision || !actual)
263
+ return;
264
+ const d = { ...status.decision, status: status.decision.status === 'unsupported' ? 'unsupported' : actual.reasoningEffort === status.effort && actual.provider === status.decision.provider && actual.model === status.decision.model ? 'applied' : 'failed', ...(actual.model ? { model: actual.model } : {}), ...(actual.provider ? { provider: actual.provider } : {}) };
265
+ mainStatus.set(session.id, { ...status, decision: d });
266
+ latest.set(session.id, d);
267
+ if (d.status === 'applied') {
268
+ applied.set(session.id, d);
269
+ requestMetadata.set(session.id, { requestSeq, appliedAt: Date.now(), ...(turn !== undefined ? { requestTurn: turn } : {}) });
270
+ }
271
+ else
272
+ applied.delete(session.id);
273
+ };
220
274
  ctx.on('session/event', (session, event) => {
221
275
  if (event.type === 'assistant/message') {
276
+ // Unchanged requests reuse the durable header and emit no request/header event.
277
+ if (!applied.has(session.id) && mainStatus.get(session.id)?.decision?.status === 'recommended') {
278
+ const header = session.snapshotEvents().findLast(e => e.type === 'request/header');
279
+ if (header)
280
+ confirmMainRequest(session, header.seq, event.data.turn);
281
+ }
222
282
  const d = applied.get(session.id);
223
283
  const data = event.data;
224
284
  if (d?.status === 'applied' && enabled(session.id) && data.message?.id && d.model) {
@@ -243,18 +303,7 @@ export function policyHost(ctx, deps) {
243
303
  requestMetadata.delete(session.id);
244
304
  const record = deps.store.get(session.id);
245
305
  if (!record?.route) {
246
- const status = mainStatus.get(session.id);
247
- const actual = event.data.header?.config;
248
- if (enabled(session.id) && status?.decision && actual) {
249
- const d = { ...status.decision, status: status.decision.status === 'unsupported' ? 'unsupported' : actual.reasoningEffort === status.effort && actual.provider === status.decision.provider && actual.model === status.decision.model ? 'applied' : 'failed', ...(actual.model ? { model: actual.model } : {}), ...(actual.provider ? { provider: actual.provider } : {}) };
250
- mainStatus.set(session.id, { ...status, decision: d });
251
- latest.set(session.id, d);
252
- if (d.status === 'applied') {
253
- applied.set(session.id, d);
254
- const data = event.data;
255
- requestMetadata.set(session.id, { requestSeq: event.seq, appliedAt: Date.now(), ...(typeof data.turn === 'number' ? { requestTurn: data.turn } : {}) });
256
- }
257
- }
306
+ confirmMainRequest(session, event.seq, event.data.turn);
258
307
  return;
259
308
  }
260
309
  const actual = event.data.header?.config;
@@ -268,6 +317,7 @@ export function policyHost(ctx, deps) {
268
317
  throw new Error('policy-v2 实际请求配置不匹配');
269
318
  });
270
319
  return {
320
+ appliedDecision: (id) => enabled(id) ? applied.get(id)?.id : undefined,
271
321
  invalidate: (id) => { epochs.set(id, epoch(id) + 1); mainStatus.delete(id); latest.delete(id); applied.delete(id); },
272
322
  status: (id) => deps.store.get(id), mainStatus: (id) => enabled(id) ? { ...mainStatus.get(id), ...(latest.get(id) ? { decision: latest.get(id) } : {}) } : undefined,
273
323
  };
@@ -27,6 +27,7 @@ declare const schema: z.ZodObject<{
27
27
  claude: "claude";
28
28
  codex: "codex";
29
29
  "dsh-magpie-connect": "dsh-magpie-connect";
30
+ "dsh-magpie-connector": "dsh-magpie-connector";
30
31
  }>;
31
32
  model: z.ZodOptional<z.ZodString>;
32
33
  reasoningEffort: z.ZodOptional<z.ZodString>;
@@ -57,6 +58,7 @@ declare const schema: z.ZodObject<{
57
58
  claude: "claude";
58
59
  codex: "codex";
59
60
  "dsh-magpie-connect": "dsh-magpie-connect";
61
+ "dsh-magpie-connector": "dsh-magpie-connector";
60
62
  }>;
61
63
  model: z.ZodString;
62
64
  reasoningEffort: z.ZodEnum<{
@@ -71,6 +73,7 @@ declare const schema: z.ZodObject<{
71
73
  claude: "claude";
72
74
  codex: "codex";
73
75
  "dsh-magpie-connect": "dsh-magpie-connect";
76
+ "dsh-magpie-connector": "dsh-magpie-connector";
74
77
  }>;
75
78
  model: z.ZodString;
76
79
  reasoningEffort: z.ZodEnum<{
@@ -2,14 +2,15 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdirSync, readFileSync, writeFileSync, renameSync, chmodSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { z } from 'zod';
5
- const route = z.object({ provider: z.enum(['claude', 'codex', 'dsh-magpie-connect']), model: z.string().min(1).max(128), reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh']) }).strict();
5
+ import { POLICY_PROVIDERS } from './policy-v2.js';
6
+ const route = z.object({ provider: z.enum(POLICY_PROVIDERS), model: z.string().min(1).max(128), reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh']) }).strict();
6
7
  const schema = z.object({
7
8
  version: z.literal(1), childId: z.string(), parentId: z.string(), rootId: z.string(),
8
9
  policyVersion: z.string(), mappingVersion: z.string(),
9
10
  status: z.enum(['recommended', 'applied', 'unsupported', 'needs-context', 'failed']),
10
11
  path: z.enum(['ordinary', 'teammate', 'fork']),
11
12
  family: z.enum(['claude', 'codex']).optional(),
12
- explicit: z.object({ provider: z.enum(['claude', 'codex', 'dsh-magpie-connect']), model: z.string().optional(), reasoningEffort: z.string().optional() }).strict(),
13
+ explicit: z.object({ provider: z.enum(POLICY_PROVIDERS), model: z.string().optional(), reasoningEffort: z.string().optional() }).strict(),
13
14
  decision: z.object({ id: z.number(), source: z.enum(['main', 'subagent']), tier: z.string(), depth: z.string(), capability: z.string().optional(), effort: z.string(), model: z.string().optional(), provider: z.string().optional(), status: z.string(), ms: z.number(), decisionProvider: z.string(), decisionModel: z.string(), confidence: z.number().min(0).max(1).optional(), capabilityConfidence: z.number().min(0).max(1).optional(), probabilities: z.record(z.string(), z.number().min(0).max(1)).optional(), capabilityProbabilities: z.record(z.string(), z.number().min(0).max(1)).optional() }).strict().optional(),
14
15
  route: route.optional(), actual: route.optional(), reason: z.string().max(240).optional(),
15
16
  }).strict();
@@ -1,8 +1,9 @@
1
1
  import type { Capability, Depth } from './jev.js';
2
2
  import { type ChildProvider } from './effort.js';
3
3
  import { type AutomaticEffort, type RoutingCatalog } from './settings-contract.js';
4
- export declare const MAGPIE_PROVIDER = "dsh-magpie-connect";
5
- export type PolicyProvider = ChildProvider | typeof MAGPIE_PROVIDER;
4
+ export declare const POLICY_PROVIDERS: readonly ["claude", "codex", "dsh-magpie-connect", "dsh-magpie-connector"];
5
+ export type PolicyProvider = typeof POLICY_PROVIDERS[number];
6
+ export declare function magpieProvider(value: string): value is 'dsh-magpie-connect' | 'dsh-magpie-connector';
6
7
  export declare function policyProvider(value: string): value is PolicyProvider;
7
8
  export declare function modelFamily(provider: string, model: string): ChildProvider | undefined;
8
9
  export declare function validatePolicyRoute(catalog: RoutingCatalog, provider: string, model: string, effort: string, cap: AutomaticEffort): void;
package/lib/policy-v2.js CHANGED
@@ -1,19 +1,20 @@
1
1
  import { targetEffort } from './effort.js';
2
2
  import { AUTO_EFFORTS } from './settings-contract.js';
3
3
  import { MODEL_ID_PATTERN } from './settings-validation.js';
4
- export const MAGPIE_PROVIDER = 'dsh-magpie-connect';
5
- export function policyProvider(value) { return value === MAGPIE_PROVIDER || value === 'claude' || value === 'codex'; }
4
+ export const POLICY_PROVIDERS = ['claude', 'codex', 'dsh-magpie-connect', 'dsh-magpie-connector'];
5
+ export function magpieProvider(value) { return value === 'dsh-magpie-connect' || value === 'dsh-magpie-connector'; }
6
+ export function policyProvider(value) { return POLICY_PROVIDERS.some(provider => provider === value); }
6
7
  export function modelFamily(provider, model) {
7
8
  if (provider === 'claude' || provider === 'codex')
8
9
  return provider;
9
- if (provider !== MAGPIE_PROVIDER)
10
+ if (!magpieProvider(provider))
10
11
  return undefined;
11
12
  const family = model.split('/')[0];
12
13
  return family === 'claude' || family === 'codex' ? family : undefined;
13
14
  }
14
15
  export function validatePolicyRoute(catalog, provider, model, effort, cap) {
15
16
  const entry = catalog.models.find(m => m.provider === provider && m.model === model);
16
- const validId = provider === MAGPIE_PROVIDER ? /^[A-Za-z0-9._:-]+(?:\/[A-Za-z0-9._:-]+)+$/.test(model) && model.length <= 128 : MODEL_ID_PATTERN.test(model);
17
+ const validId = magpieProvider(provider) ? /^[A-Za-z0-9._:-]+(?:\/[A-Za-z0-9._:-]+)+$/.test(model) && model.length <= 128 : MODEL_ID_PATTERN.test(model);
17
18
  if (!policyProvider(provider) || !validId || !entry || !AUTO_EFFORTS.includes(effort) || AUTO_EFFORTS.indexOf(effort) > AUTO_EFFORTS.indexOf(cap) || !entry.reasoningEfforts.includes(effort) || catalog.childAllowlist.status !== 'enabled' || !catalog.childAllowlist.allowedModels.some(m => m.provider === provider && m.model === model))
18
19
  throw new Error('策略路由不在当前目录/允许列表或 effort 上限内;暂停委派并询问用户');
19
20
  }
@@ -42,10 +43,10 @@ export function policyEffort(model, depth, supported, cap, defaultEffort) {
42
43
  return AUTO_EFFORTS.indexOf(chosen) > AUTO_EFFORTS.indexOf(cap) ? allowed.at(-1) : chosen;
43
44
  }
44
45
  export function choosePolicyRoute(provider, capability, depth, catalog, cap, explicit = {}, family) {
45
- const selectedFamily = provider === MAGPIE_PROVIDER ? family : provider;
46
+ const selectedFamily = magpieProvider(provider) ? family : provider;
46
47
  if (!explicit.model && !selectedFamily)
47
48
  throw new Error('unsupported:没有已定义的模型能力家族;请显式指定模型');
48
- const model = explicit.model ?? `${provider === MAGPIE_PROVIDER ? `${selectedFamily}/` : ''}${POLICY_MODELS[selectedFamily][capability]}`;
49
+ const model = explicit.model ?? `${magpieProvider(provider) ? `${selectedFamily}/` : ''}${POLICY_MODELS[selectedFamily][capability]}`;
49
50
  const entry = catalog.models.find(m => m.provider === provider && m.model === model);
50
51
  const reasoningEffort = explicit.reasoningEffort ?? policyEffort(model, depth, entry?.reasoningEfforts ?? [], cap, entry?.defaultEffort);
51
52
  validatePolicyRoute(catalog, provider, model, reasoningEffort, cap);
@@ -1,6 +1,7 @@
1
1
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
2
2
  import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
3
3
  import { DEFAULT_SETTINGS, SettingsValidationError } from './settings.js';
4
+ import { magpieProvider } from './policy-v2.js';
4
5
  import rubric from './rubric.json' with { type: 'json' };
5
6
  import { JevClient } from './jev.js';
6
7
  import { DECISION_SERVICES } from './decision-service.js';
@@ -10,7 +11,7 @@ export function settingsHost(ctx, store) {
10
11
  const environment = launchEnvironmentOf(ctx);
11
12
  const credentials = () => ctx.get('credentials');
12
13
  const catalog = async (includeMagpie = store.read().settings.policy === 'policy-v2') => {
13
- const models = (await Promise.all(ctx.llm.listProviders().filter(p => p.id === 'claude' || p.id === 'codex' || (includeMagpie && p.id === 'dsh-magpie-connect')).map(async (provider) => {
14
+ const models = (await Promise.all(ctx.llm.listProviders().filter(p => p.id === 'claude' || p.id === 'codex' || (includeMagpie && magpieProvider(p.id))).map(async (provider) => {
14
15
  try {
15
16
  return await Promise.all((await ctx.llm.listModels(provider.id)).map(async (model) => {
16
17
  try {
@@ -31,7 +32,7 @@ export function settingsHost(ctx, store) {
31
32
  try {
32
33
  if (service) {
33
34
  const current = service.current();
34
- return { models, childAllowlist: { status: current.enabled ? 'enabled' : 'disabled', allowedModels: current.allowedModels.filter(r => r.provider === 'claude' || r.provider === 'codex' || (includeMagpie && r.provider === 'dsh-magpie-connect')) } };
35
+ return { models, childAllowlist: { status: current.enabled ? 'enabled' : 'disabled', allowedModels: current.allowedModels.filter(r => r.provider === 'claude' || r.provider === 'codex' || (includeMagpie && magpieProvider(r.provider))) } };
35
36
  }
36
37
  }
37
38
  catch { /* An unavailable policy must never broaden the allowed routes. */ }
@@ -5,7 +5,7 @@ import { DEFAULT_SETTINGS } from './settings-contract.js';
5
5
  import { validateChildRoute } from './settings-validation.js';
6
6
  import { CATEGORY_HINTS } from './jev.js';
7
7
  import { buildState } from './context.js';
8
- import { choosePolicyRoute, modelFamily } from './policy-v2.js';
8
+ import { choosePolicyRoute, magpieProvider, modelFamily, POLICY_PROVIDERS } from './policy-v2.js';
9
9
  import { policyVersion } from './jev.js';
10
10
  /** The ordinary session a caller belongs to; undefined when any link is no longer live (treated as MANUAL). */
11
11
  function rootOf(caller, agents) {
@@ -27,7 +27,7 @@ export function routeSubagentTool(jev, deps) {
27
27
  timeoutMs: 5000,
28
28
  parameters: {
29
29
  task: { type: 'string', required: true, description: 'The task the sub-agent will do.' },
30
- provider: { type: 'string', required: true, enum: ['claude', 'codex', 'dsh-magpie-connect'], description: 'Exact transport provider. Magpie is available only under policy-v2.' },
30
+ provider: { type: 'string', required: true, enum: [...POLICY_PROVIDERS], description: 'Exact transport provider. Magpie is available only under policy-v2.' },
31
31
  model: { type: 'string', description: 'Sub-agent model already chosen by the user or lead; when given, only reasoning_effort is routed.' },
32
32
  reasoning_effort: { type: 'string', description: 'Explicit effort constraint for policy-v2; preserved and validated, never silently replaced.' },
33
33
  family: { type: 'string', enum: ['claude', 'codex'], description: 'Magpie automatic capability family; required when no model is explicitly supplied.' },
@@ -76,7 +76,7 @@ export function routeSubagentTool(jev, deps) {
76
76
  const route = choosePolicyRoute(args.provider, result.capability ?? 'bounded', result.depth, await deps.catalog(), deps.readSettings().effortCap, explicit, args.family ?? (args.model ? modelFamily(args.provider, args.model) : undefined));
77
77
  return { status: 'recommended', policyVersion, ...(result.capability ? { capability: result.capability } : {}), depth: result.depth, provider: route.provider, model: route.model, reasoning_effort: route.reasoningEffort };
78
78
  }
79
- if (args.provider === 'dsh-magpie-connect')
79
+ if (magpieProvider(args.provider))
80
80
  throw new Error('Magpie 路由需显式选择 policy-v2;原策略不变');
81
81
  const result = await jev.judge(buildState({ text: args.task }), exec.signal);
82
82
  if (result.kind === 'cancelled')
@@ -0,0 +1,25 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ /** One main-session model selection as the AUTO support gate needs it. */
4
+ export interface MainSelection {
5
+ provider: string;
6
+ model: string;
7
+ }
8
+ /**
9
+ * AUTO only manages the Claude / GPT model families.
10
+ *
11
+ * The transport prefix (`claude/...`, `codex/...` on Magpie) is not part of the
12
+ * family name, so it is stripped before matching. Every other family — DeepSeek,
13
+ * GLM, Grok, Gemini, Kimi, Qwen, ... — is explicitly out of scope and locks the
14
+ * session to MANUAL instead of being guessed at.
15
+ */
16
+ export declare function autoSupportedModel(model: string | undefined): boolean;
17
+ /** An unknown selection must never lock a session: only a proven mismatch does. */
18
+ export declare function mainSupported(selection: MainSelection | undefined): boolean;
19
+ /**
20
+ * The main session's current model selection, most-authoritative signal first:
21
+ * the official pending pick (`model/selection` projection), then the last actual
22
+ * request header, then the creation-time agent options. Undefined when none can
23
+ * be read; callers treat that as "unknown, do not lock".
24
+ */
25
+ export declare function currentMainSelection(ctx: Context, agent: Agent): MainSelection | undefined;
package/lib/support.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * AUTO only manages the Claude / GPT model families.
3
+ *
4
+ * The transport prefix (`claude/...`, `codex/...` on Magpie) is not part of the
5
+ * family name, so it is stripped before matching. Every other family — DeepSeek,
6
+ * GLM, Grok, Gemini, Kimi, Qwen, ... — is explicitly out of scope and locks the
7
+ * session to MANUAL instead of being guessed at.
8
+ */
9
+ export function autoSupportedModel(model) {
10
+ if (typeof model !== 'string' || model.length === 0)
11
+ return false;
12
+ const bare = model.includes('/') ? model.slice(model.lastIndexOf('/') + 1) : model;
13
+ return /^(?:claude|gpt)-/i.test(bare);
14
+ }
15
+ /** An unknown selection must never lock a session: only a proven mismatch does. */
16
+ export function mainSupported(selection) {
17
+ return selection === undefined || autoSupportedModel(selection.model);
18
+ }
19
+ /**
20
+ * The main session's current model selection, most-authoritative signal first:
21
+ * the official pending pick (`model/selection` projection), then the last actual
22
+ * request header, then the creation-time agent options. Undefined when none can
23
+ * be read; callers treat that as "unknown, do not lock".
24
+ */
25
+ export function currentMainSelection(ctx, agent) {
26
+ try {
27
+ const service = ctx.get('sessionProjections');
28
+ const pending = service?.stateOf(agent.session, 'modelSelection')?.pending;
29
+ if (pending && typeof pending.provider === 'string' && typeof pending.model === 'string')
30
+ return { provider: pending.provider, model: pending.model };
31
+ }
32
+ catch { /* optional projection: fall through to the durable header */ }
33
+ const header = agent.session.requestHeader()?.config;
34
+ if (header && typeof header.provider === 'string' && typeof header.model === 'string')
35
+ return { provider: header.provider, model: header.model };
36
+ const options = agent.options;
37
+ if (typeof options.provider === 'string' && typeof options.model === 'string')
38
+ return { provider: options.provider, model: options.model };
39
+ return undefined;
40
+ }
@@ -0,0 +1,78 @@
1
+ type Failure = 'decode-failed' | 'tail-incomplete' | 'parse-failed' | 'missing-log';
2
+ export declare function buildReport(home: string, options?: {
3
+ since?: string;
4
+ }): {
5
+ schema: number;
6
+ since: string | null;
7
+ failures: Record<Failure, number>;
8
+ excluded: {
9
+ child: number;
10
+ seeded: number;
11
+ };
12
+ coverage: {
13
+ userTurns: number;
14
+ turnsWithoutRecord: number;
15
+ unlinkedBindings: number;
16
+ autoSessionsWithoutRecord: number;
17
+ };
18
+ groups: {
19
+ version: string;
20
+ subgroups: {
21
+ model: string;
22
+ depth: string;
23
+ decisions: number;
24
+ sessions: number;
25
+ calls: number;
26
+ callsPerDecision: {
27
+ median: number;
28
+ max: number;
29
+ };
30
+ tokens: Record<string, {
31
+ sum: number | null;
32
+ mean: number | null;
33
+ present: number;
34
+ calls: number;
35
+ }>;
36
+ byEffort: {
37
+ [k: string]: {
38
+ decisions: number;
39
+ calls: number;
40
+ };
41
+ };
42
+ feedback: Record<string, number>;
43
+ implicit: {
44
+ counts: {
45
+ explicit: number;
46
+ negative: number;
47
+ 'implicit-ok': number;
48
+ inconclusive: number;
49
+ unobserved: number;
50
+ };
51
+ explicit: {
52
+ conflict: number;
53
+ };
54
+ exposedExplicit: {
55
+ conflict: number;
56
+ };
57
+ negative: {
58
+ 'aborted-user': number;
59
+ 'manual-switch': number;
60
+ };
61
+ };
62
+ conclusion: string;
63
+ }[];
64
+ }[];
65
+ referenceBeforeAuto: {
66
+ model: string;
67
+ calls: number;
68
+ sessions: number;
69
+ effort: {
70
+ [k: string]: number;
71
+ };
72
+ autoEffort: {
73
+ [k: string]: number;
74
+ };
75
+ }[];
76
+ caveats: string[];
77
+ };
78
+ export {};