dsh-autotier 0.1.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.
- package/AGENTS.md +93 -0
- package/CHANGELOG.md +85 -0
- package/LICENSE +201 -0
- package/README.es.md +247 -0
- package/README.hi.md +241 -0
- package/README.md +245 -0
- package/README.pt.md +246 -0
- package/README.zh.md +221 -0
- package/SECURITY.md +55 -0
- package/THIRD_PARTY_NOTICES.md +63 -0
- package/cordis.patch.yml +125 -0
- package/docs/preset-row.md +61 -0
- package/docs/supporting-lanes.md +45 -0
- package/lib/index.js +2848 -0
- package/lib/types/command.d.ts +17 -0
- package/lib/types/command.d.ts.map +1 -0
- package/lib/types/config.d.ts +94 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/guard-rules.d.ts +97 -0
- package/lib/types/guard-rules.d.ts.map +1 -0
- package/lib/types/guard.d.ts +70 -0
- package/lib/types/guard.d.ts.map +1 -0
- package/lib/types/index.d.ts +60 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/intent.d.ts +179 -0
- package/lib/types/intent.d.ts.map +1 -0
- package/lib/types/judge.d.ts +50 -0
- package/lib/types/judge.d.ts.map +1 -0
- package/lib/types/policy.d.ts +109 -0
- package/lib/types/policy.d.ts.map +1 -0
- package/lib/types/routing.d.ts +135 -0
- package/lib/types/routing.d.ts.map +1 -0
- package/lib/types/schema.d.ts +134 -0
- package/lib/types/schema.d.ts.map +1 -0
- package/lib/types/service.d.ts +67 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/state.d.ts +46 -0
- package/lib/types/state.d.ts.map +1 -0
- package/lib/types/tiers.d.ts +103 -0
- package/lib/types/tiers.d.ts.map +1 -0
- package/lib/types/tools.d.ts +26 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/types.d.ts +96 -0
- package/lib/types/types.d.ts.map +1 -0
- package/package.json +179 -0
- package/src/command.ts +73 -0
- package/src/config.ts +358 -0
- package/src/guard-rules.ts +303 -0
- package/src/guard.ts +285 -0
- package/src/index.ts +149 -0
- package/src/intent.ts +484 -0
- package/src/judge.ts +150 -0
- package/src/policy.ts +246 -0
- package/src/routing.ts +575 -0
- package/src/schema.ts +295 -0
- package/src/service.ts +131 -0
- package/src/state.ts +134 -0
- package/src/tiers.ts +212 -0
- package/src/tools.ts +128 -0
- package/src/types.ts +120 -0
package/src/policy.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routing decision state machine: it turns one classified intent plus the
|
|
3
|
+
* per-agent runtime state into the tier to apply, and owns the TTL semantics of
|
|
4
|
+
* escalation, fallback and the judge cooldown. Pure and synchronous — the
|
|
5
|
+
* asynchronous judge call lives in `judge.ts`.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-autotier/policy
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ResolvedConfig } from './config.ts'
|
|
11
|
+
import type { IntentInput, IntentResult, RuleHit } from './intent.ts'
|
|
12
|
+
import { advanceFallback, fallbackActive, type FallbackRecord } from './tiers.ts'
|
|
13
|
+
import type { RouteSource, RoutingMode, TierId } from './types.ts'
|
|
14
|
+
|
|
15
|
+
/** Mutable per-agent routing state. Never persisted: it is per-process runtime. */
|
|
16
|
+
export interface RouteState {
|
|
17
|
+
/** The decision for the newest user input, reused by every step of its turn. */
|
|
18
|
+
decision: IntentResult | undefined
|
|
19
|
+
/** The classifier input the decision was computed from. */
|
|
20
|
+
input: IntentInput | undefined
|
|
21
|
+
/** Session-level override set by `/tier`; `undefined` = follow the configuration. */
|
|
22
|
+
override: RoutingMode | undefined
|
|
23
|
+
/** The tier actually applied to the last request (hysteresis anchor). */
|
|
24
|
+
appliedTier: TierId | undefined
|
|
25
|
+
/** The source that produced `appliedTier`; hysteresis only damps classifier-driven changes. */
|
|
26
|
+
appliedSource: RouteSource | undefined
|
|
27
|
+
/** Plan mode as last observed. */
|
|
28
|
+
planActive: boolean
|
|
29
|
+
/** Failure escalation. */
|
|
30
|
+
escalation: { count: number; signature: string; until: number; rung: number; lastAt: number } | undefined
|
|
31
|
+
/** Fallback-chain position (scoped to one tier). */
|
|
32
|
+
fallback: FallbackRecord | undefined
|
|
33
|
+
/** Judge resilience. */
|
|
34
|
+
judge: { failures: number; lastCall: number }
|
|
35
|
+
/** Attempt-first band: the strong review has already run for this input. */
|
|
36
|
+
verified: boolean
|
|
37
|
+
/** The fingerprint that owes one strong review after a cheap-run signal. */
|
|
38
|
+
reviewOwedFor: string | undefined
|
|
39
|
+
/** The posterior exploration roll, taken once per user input. */
|
|
40
|
+
probe: 'strong' | 'cheap' | undefined
|
|
41
|
+
/** How many calls the guard denied for this agent. */
|
|
42
|
+
denials: number
|
|
43
|
+
/** The last rule the guard fired, for `/tier status`. */
|
|
44
|
+
lastDenial: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A fresh per-agent state. */
|
|
48
|
+
export function createRouteState(): RouteState {
|
|
49
|
+
return {
|
|
50
|
+
decision: undefined,
|
|
51
|
+
input: undefined,
|
|
52
|
+
override: undefined,
|
|
53
|
+
appliedTier: undefined,
|
|
54
|
+
appliedSource: undefined,
|
|
55
|
+
planActive: false,
|
|
56
|
+
escalation: undefined,
|
|
57
|
+
fallback: undefined,
|
|
58
|
+
judge: { failures: 0, lastCall: 0 },
|
|
59
|
+
verified: false,
|
|
60
|
+
reviewOwedFor: undefined,
|
|
61
|
+
probe: undefined,
|
|
62
|
+
denials: 0,
|
|
63
|
+
lastDenial: '',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One routing decision with provenance. */
|
|
68
|
+
export interface Decision {
|
|
69
|
+
readonly tier: TierId
|
|
70
|
+
readonly source: RouteSource
|
|
71
|
+
readonly reason: string
|
|
72
|
+
readonly confidence: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Inputs to {@link decideTier}. */
|
|
76
|
+
export interface DecideInput {
|
|
77
|
+
readonly config: ResolvedConfig
|
|
78
|
+
readonly state: RouteState
|
|
79
|
+
readonly intent: IntentResult
|
|
80
|
+
readonly rule: RuleHit | null
|
|
81
|
+
/** Session/plugin override; `undefined` means the configured routing mode. */
|
|
82
|
+
readonly override: RoutingMode | undefined
|
|
83
|
+
readonly now: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether the current failure escalation is still in force. */
|
|
87
|
+
export function escalationActive(state: RouteState, now: number): boolean {
|
|
88
|
+
return state.escalation !== undefined && state.escalation.until > now
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Apply the double-threshold hysteresis to a classifier-driven change. The
|
|
93
|
+
* anchor is only honoured when the applied tier itself came from the
|
|
94
|
+
* classifier: an escalation, plan-mode, rule or manual decision is a deliberate
|
|
95
|
+
* instruction, so returning from it must not be damped (otherwise a session
|
|
96
|
+
* that escalated once never returns to the cheap tier).
|
|
97
|
+
*/
|
|
98
|
+
function withHysteresis(state: RouteState, proposed: TierId, confidence: number, config: ResolvedConfig): TierId {
|
|
99
|
+
const applied = state.appliedTier
|
|
100
|
+
if (applied === undefined || applied === proposed) return proposed
|
|
101
|
+
const anchorSource = state.appliedSource
|
|
102
|
+
if (anchorSource !== 'judge' && anchorSource !== 'posterior' && anchorSource !== 'default') return proposed
|
|
103
|
+
const { toStrong, toCheap } = config.intent.hysteresis
|
|
104
|
+
if (proposed === 'strong' && confidence < toStrong) return applied
|
|
105
|
+
if (proposed === 'cheap' && confidence >= toCheap) return applied
|
|
106
|
+
return proposed
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the tier for the current step.
|
|
111
|
+
*
|
|
112
|
+
* Precedence (highest first): explicit override, active failure escalation,
|
|
113
|
+
* plan mode, declarative rule, fingerprint posterior, classifier verdict.
|
|
114
|
+
* Hysteresis applies only to the classifier verdict, so an explicit override
|
|
115
|
+
* or an escalation takes effect immediately.
|
|
116
|
+
*
|
|
117
|
+
* @param input - the live state and the classification.
|
|
118
|
+
* @returns the decision with its provenance.
|
|
119
|
+
*/
|
|
120
|
+
export function decideTier(input: DecideInput): Decision {
|
|
121
|
+
const { state, config, intent, rule, override, now } = input
|
|
122
|
+
if (override === 'strong' || override === 'cheap') {
|
|
123
|
+
return { tier: override, source: 'manual', reason: `/tier ${override}`, confidence: 1 }
|
|
124
|
+
}
|
|
125
|
+
if (escalationActive(state, now)) {
|
|
126
|
+
return {
|
|
127
|
+
tier: 'strong',
|
|
128
|
+
source: 'escalation',
|
|
129
|
+
reason: `escalated after ${String(state.escalation?.count ?? 0)} failure(s)`,
|
|
130
|
+
confidence: 1,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (state.planActive) {
|
|
134
|
+
return { tier: 'strong', source: 'plan-mode', reason: 'plan mode is active', confidence: 1 }
|
|
135
|
+
}
|
|
136
|
+
if (fallbackActive(state.fallback, now)) {
|
|
137
|
+
return {
|
|
138
|
+
tier: intent.tier,
|
|
139
|
+
source: 'fallback',
|
|
140
|
+
reason: `fallback chain entry ${String((state.fallback?.index ?? 0) + 1)}`,
|
|
141
|
+
confidence: intent.confidence,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (rule !== null) {
|
|
145
|
+
return { tier: rule.tier, source: 'rule', reason: `rule ${rule.id}`, confidence: 1 }
|
|
146
|
+
}
|
|
147
|
+
// The posterior opinion (including its exploration roll) is taken once per
|
|
148
|
+
// user input by the router and stored on the state, so a tool-loop step
|
|
149
|
+
// cannot re-roll it into a different tier.
|
|
150
|
+
const posterior = state.probe ?? null
|
|
151
|
+
if (posterior !== null) {
|
|
152
|
+
return {
|
|
153
|
+
tier: posterior,
|
|
154
|
+
source: 'posterior',
|
|
155
|
+
reason: `fingerprint ${intent.fingerprint} posterior`,
|
|
156
|
+
confidence: 1,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const tier = withHysteresis(state, intent.tier, intent.confidence, config)
|
|
160
|
+
const source: RouteSource = intent.shortCircuit !== undefined ? 'rule' : 'judge'
|
|
161
|
+
const reason = tier === intent.tier
|
|
162
|
+
? intent.reasons.join('; ')
|
|
163
|
+
: `hysteresis kept ${tier} (proposed ${intent.tier} at confidence ${intent.confidence.toFixed(2)})`
|
|
164
|
+
return { tier, source, reason, confidence: intent.confidence }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Whether the low-confidence judge should be consulted for this input. */
|
|
168
|
+
export function judgeNeeded(
|
|
169
|
+
config: ResolvedConfig,
|
|
170
|
+
state: RouteState,
|
|
171
|
+
intent: IntentResult,
|
|
172
|
+
rule: RuleHit | null,
|
|
173
|
+
now: number,
|
|
174
|
+
): boolean {
|
|
175
|
+
if (!config.intent.judge.enabled) return false
|
|
176
|
+
if (rule !== null) return false
|
|
177
|
+
if (intent.shortCircuit !== undefined) return false
|
|
178
|
+
if (intent.confidence >= config.intent.ruleThreshold) return false
|
|
179
|
+
if (state.judge.failures >= config.intent.judge.unavailableSkip) return false
|
|
180
|
+
return now - state.judge.lastCall >= config.intent.judge.cooldownMs
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Whether the middle band should start cheap and verify on a signal. */
|
|
184
|
+
export function attemptBandApplies(config: ResolvedConfig, intent: IntentResult): boolean {
|
|
185
|
+
if (!config.intent.attemptBand.enabled) return false
|
|
186
|
+
if (intent.shortCircuit !== undefined) return false
|
|
187
|
+
return intent.confidence >= config.intent.attemptBand.tauLow && intent.confidence < config.intent.ruleThreshold
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Record one judge call attempt. */
|
|
191
|
+
export function noteJudgeCall(state: RouteState, now: number, ok: boolean): void {
|
|
192
|
+
state.judge.lastCall = now
|
|
193
|
+
state.judge.failures = ok ? 0 : state.judge.failures + 1
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Record one failure against the escalation counter.
|
|
198
|
+
* @param state - the agent's state.
|
|
199
|
+
* @param signature - the failure signature (`code|fingerprint`); only identical
|
|
200
|
+
* signatures accumulate when `escalation.signature` is enabled.
|
|
201
|
+
* @param config - the resolved configuration.
|
|
202
|
+
* @param now - current epoch millis.
|
|
203
|
+
* @returns whether this failure escalated the tier.
|
|
204
|
+
*/
|
|
205
|
+
export function noteFailure(state: RouteState, signature: string, config: ResolvedConfig, now: number): boolean {
|
|
206
|
+
const current = state.escalation
|
|
207
|
+
const sameSignature = config.escalation.signature ? current?.signature === signature : true
|
|
208
|
+
const withinWindow = current !== undefined && now - current.lastAt <= config.escalation.windowMs
|
|
209
|
+
const count = sameSignature && current !== undefined && withinWindow ? current.count + 1 : 1
|
|
210
|
+
const escalated = count >= config.escalation.threshold
|
|
211
|
+
state.escalation = {
|
|
212
|
+
count,
|
|
213
|
+
signature: config.escalation.signature ? signature : '',
|
|
214
|
+
until: escalated ? now + config.escalation.ttlMs : (current?.until ?? 0),
|
|
215
|
+
rung: escalated ? (current?.rung ?? 0) + 1 : (current?.rung ?? 0),
|
|
216
|
+
lastAt: now,
|
|
217
|
+
}
|
|
218
|
+
return escalated
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Clear an expired escalation lazily. A record that never escalated keeps its window count. */
|
|
222
|
+
export function clearExpiredEscalation(state: RouteState, now: number): void {
|
|
223
|
+
if (state.escalation !== undefined && state.escalation.until > 0 && state.escalation.until <= now) {
|
|
224
|
+
state.escalation = undefined
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Advance the agent's fallback chain for one tier after an unusable route.
|
|
230
|
+
* @returns whether a chain entry was taken (false = chain exhausted).
|
|
231
|
+
*/
|
|
232
|
+
export function noteFallback(
|
|
233
|
+
state: RouteState,
|
|
234
|
+
tier: TierId,
|
|
235
|
+
chainLength: number,
|
|
236
|
+
config: ResolvedConfig,
|
|
237
|
+
now: number,
|
|
238
|
+
): boolean {
|
|
239
|
+
const next = advanceFallback(state.fallback, tier, chainLength, now, config.escalation.fallbackTtlMs)
|
|
240
|
+
if (next === null) {
|
|
241
|
+
if (state.fallback?.tier === tier) state.fallback = undefined
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
state.fallback = next
|
|
245
|
+
return true
|
|
246
|
+
}
|