policy/algo.bend

cswap-bend-cli@0.1.0.0 · 163 lines · 22 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/policy/types.bend as T
3import cswap-bend-cli@0.1.0.0/policy/usage.bend as U
4import cswap-bend-cli@0.1.0.0/policy/rank.bend as R
5import cswap-bend-cli@0.1.0.0/policy/exhaust.bend as E
6import cswap-bend-cli@0.1.0.0/policy/decide.bend as D
7import cswap-bend-cli@0.1.0.0/policy/safe.bend as SF
8
9# Behaviour profiles, part 2: one tick (or one manual rotation) under a
10# strategy. `best` is `D.decide`, unchanged. The others share the gates below
11# and differ only in the ranking key (`SF.key`):
12#
13#   gates, in order: unmanaged active account, active API key, unknown active
14#   headroom (never switches, there is no failover), and for an automatic tick
15#   the threshold and the cooldown; then `SF.pick` over the safe candidates.
16#
17# A rotation (`VRotate`, the manual `switch` with no target) skips the
18# threshold and the cooldown: the user asked to move now.
19
20type Via is Data:
21  VAuto{}
22  VRotate{}
23
24def via.auto(+v: Via) -> Bool:
25  match v:
26    case VAuto{}:
27      True{}
28    case VRotate{}:
29      False{}
30
31def drain(+algo: SF.Algo) -> Bool:
32  match algo:
33    case SF.ADrainFirst{}:
34      True{}
35    case o:
36      False{}
37
38def need.under(+under: Bool, +thr: Nat) -> Nat:
39  match under:
40    case True{}:
41      Nat.add(Nat.sub(T.full(), thr), 1n)
42    case False{}:
43      1n
44
45# The least headroom a target needs: drain-first and a proactive automatic
46# switch must land below the threshold; an at-limit switch or a rotation takes
47# any headroom.
48def need.of(+algo: SF.Algo, +via: Via, +a: Nat, +thr: Nat) -> Nat:
49  need.under(drain(algo) || (via.auto(via) && Nat.is_gt(a, 0n)), thr)
50
51def mode.of(+algo: SF.Algo) -> T.Mode:
52  match algo:
53    case SF.ASoonestReset{}:
54      T.MReset{}
55    case o:
56      T.MHead{}
57
58def trig.via(+via: Via, +a: Nat) -> T.Trigger:
59  match via:
60    case VAuto{}:
61      D.trig.of(a)
62    case VRotate{}:
63      T.Proactive{}
64
65def sw(+m: Maybe<&2, SF.Cd>, +algo: SF.Algo, +t: T.Trigger, +next: T.Decision) -> T.Decision:
66  match m:
67    case None{}:
68      next
69    case Some{c}:
70      match c:
71        case SF.Cd{slot, k1, k2, h}:
72          T.Switch{slot, t, T.trigger.name(t), h, mode.of(algo), None{}, None{}}
73
74# Nothing qualified: the reason `best` would give. The decision is rebuilt as
75# a NoSwitch whatever `d` is, so no proof has to look inside `D.none.cf`.
76def nosw(+d: T.Decision) -> T.Decision:
77  match d:
78    case T.NoSwitch{r, detail}:
79      T.NoSwitch{r, detail}
80    case o:
81      T.NoSwitch{T.NoCandidates{}, ""}
82
83def go.pick(+algo: SF.Algo, +via: Via, +a: Nat, +e: D.Env) -> T.Decision:
84  match e:
85    case D.Env{active, as, us, now, cfg, st, mem}:
86      sw(SF.pick(algo, as, active, us, now, D.cfg.models(cfg), need.of(algo, via, a, D.cfg.threshold(cfg))), algo, trig.via(via, a),
87        nosw(D.none.cf(T.Proactive{}, None{}, as, active, us, now, D.cfg.models(cfg))))
88
89def go.cool(+cool: Bool, +algo: SF.Algo, +via: Via, +a: Nat, +e: D.Env) -> T.Decision:
90  match cool:
91    case True{}:
92      T.NoSwitch{T.Cooldown{}, ""}
93    case False{}:
94      go.pick(algo, via, a, e)
95
96# At limit ignores the cooldown; a proactive switch honours it.
97def go.auto(+a: Nat, +algo: SF.Algo, +via: Via, +e: D.Env) -> T.Decision:
98  match a:
99    case 0n:
100      go.pick(algo, via, a, e)
101    case 1n+p:
102      go.cool(D.in_cooldown(D.st.last_at(D.env.st(e)), D.env.now(e), D.cfg.cooldown(D.env.cfg(e))), algo, via, a, e)
103
104def go.via(+algo: SF.Algo, +via: Via, +a: Nat, +e: D.Env) -> T.Decision:
105  match via:
106    case VAuto{}:
107      go.auto(a, algo, via, e)
108    case VRotate{}:
109      go.pick(algo, via, a, e)
110
111def go.below(+below: Bool, +algo: SF.Algo, +via: Via, +a: Nat, +e: D.Env) -> T.Decision:
112  match below:
113    case True{}:
114      T.NoSwitch{T.BelowThreshold{}, D.below.detail(a, D.env.cfg(e))}
115    case False{}:
116      go.via(algo, via, a, e)
117
118def go.known(+ah: Maybe<&2, Nat>, +algo: SF.Algo, +via: Via, +e: D.Env) -> T.Decision:
119  match ah:
120    case None{}:
121      T.NoSwitch{T.ActiveUsageUnknown{}, "headroom unknown; a profile strategy never switches away from an account it cannot read (use 'cswap switch NUM')"}
122    case Some{a}:
123      go.below(via.auto(via) && Nat.is_lt(Nat.sub(T.full(), a), D.cfg.threshold(D.env.cfg(e))), algo, via, a, e)
124
125def go.run(+algo: SF.Algo, +via: Via, +e: D.Env) -> T.Decision:
126  go.known(D.env.ah(e), algo, via, e)
127
128def go.kind(+k: T.AcctKind, +algo: SF.Algo, +via: Via, +e: D.Env) -> T.Decision:
129  match k:
130    case T.ApiKey{}:
131      T.NoSwitch{T.ActiveApiKey{}, "API-key accounts have no quota to watch"}
132    case o:
133      go.run(algo, via, e)
134
135def go.acct(+m: Maybe<&2, T.Account>, +algo: SF.Algo, +via: Via, +e: D.Env) -> T.Decision:
136  match m:
137    case None{}:
138      T.NoSwitch{T.UnmanagedActive{}, "run 'cswap --add-account' to include it in rotation"}
139    case Some{a}:
140      match a:
141        case T.Account{slot, email, kind, disabled}:
142          go.kind(kind, algo, via, e)
143
144def core(+algo: SF.Algo, +via: Via, +e: D.Env) -> T.Decision:
145  go.acct(U.account.of(D.env.active(e), D.env.as(e)), algo, via, e)
146
147# One autoswitch tick under `algo`.
148def decide(+algo: SF.Algo, +active: Nat, +accounts: List<&2, T.Account>, +usage: List<&2, T.Usage>, +now: Nat, +cfg: T.Config, +st: T.State, +mem: T.Mem) -> T.Decision:
149  match algo:
150    case SF.ABest{}:
151      D.decide(active, accounts, usage, now, cfg, st, mem)
152    case o:
153      core(o, VAuto{}, D.Env{active, accounts, usage, now, cfg, st, mem})
154
155# A manual rotation under `algo`: the target now, whatever the active's
156# utilization. `best` ranks by headroom, as `sticky` does.
157def rotate(+algo: SF.Algo, +active: Nat, +accounts: List<&2, T.Account>, +usage: List<&2, T.Usage>, +now: Nat, +cfg: T.Config) -> T.Decision:
158  core(algo, VRotate{}, D.Env{active, accounts, usage, now, cfg, T.fresh_state(), T.fresh_mem()})
159
160# One tick under the strategy a name stands for; anything else (best,
161# consume-first) is the reference engine.
162def decide_cfg(+name: String, +active: Nat, +accounts: List<&2, T.Account>, +usage: List<&2, T.Usage>, +now: Nat, +cfg: T.Config, +st: T.State, +mem: T.Mem) -> T.Decision:
163  decide(Maybe.default(&2, SF.Algo, SF.parse(name), SF.ABest{}), active, accounts, usage, now, cfg, st, mem)