@chatpanel/events 0.102.0 → 0.104.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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +4 -5
- package/pdf-layout.js +7 -253
- package/policy.js +1 -0
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/registry.js
CHANGED
|
@@ -1,240 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// This is the runtime half of the capability contract: `requires` / `provides` in a
|
|
4
|
-
// declaration (capability.js) only mean something because something binds them here.
|
|
5
|
-
//
|
|
6
|
-
// WHY THIS AND NOT CORDIS. We need exactly two behaviours — effects that carry their own
|
|
7
|
-
// inverse, and dependents that unwind when a capability withdraws and re-arm when it
|
|
8
|
-
// returns. That is roughly 15% of Cordis's surface; we need neither its plugin registry,
|
|
9
|
-
// its Proxy-based ctx, its config schema, its logger nor HMR. At ~200 lines this costs
|
|
10
|
-
// nothing against a first-paint budget that is a release gate, has no CSP surface, and
|
|
11
|
-
// ports to every runtime. The concepts port; the framework does not.
|
|
12
|
-
//
|
|
13
|
-
// THE TWO RULES THAT MATTER, and the bug class each prevents:
|
|
14
|
-
//
|
|
15
|
-
// 1. LIFO disposal. Inverses run in reverse order of registration, so each one meets
|
|
16
|
-
// the state its own application produced. Anything else hands an inverse a state it
|
|
17
|
-
// was not built for.
|
|
18
|
-
//
|
|
19
|
-
// 2. Dependents deactivate BEFORE a provider's binding is removed. A component being
|
|
20
|
-
// torn down because its provider is going away is running its own teardown, and
|
|
21
|
-
// that teardown frequently NEEDS the very capability being withdrawn — closing a
|
|
22
|
-
// connection pool means handing connections back to whatever provided them. Remove
|
|
23
|
-
// the binding first and the teardown reaches for something already gone. This is
|
|
24
|
-
// the orphaned-monitor / dangling-observer bug class, structurally.
|
|
25
|
-
|
|
26
|
-
const STATE = Object.freeze({ INACTIVE: 'inactive', ACTIVE: 'active' });
|
|
27
|
-
|
|
28
|
-
let nextId = 0;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @param onEvent optional hook — `{ event, name, key }`. Kept deliberately independent
|
|
32
|
-
* of the event schema so the registry has no dependency on the log; the caller
|
|
33
|
-
* maps these to `capability.activated` / `capability.revoked`.
|
|
34
|
-
*/
|
|
35
|
-
export function createRegistry({ onEvent = null } = {}) {
|
|
36
|
-
const bindings = new Map(); // key -> { value, providerId }
|
|
37
|
-
const components = new Map(); // id -> record
|
|
38
|
-
let settling = false;
|
|
39
|
-
let disposed = false;
|
|
40
|
-
let withdrawing = false; // see withdrawSync — suspends settling inside rule 2's window
|
|
41
|
-
|
|
42
|
-
const emit = (event, detail) => { if (onEvent) onEvent({ event, ...detail }); };
|
|
43
|
-
|
|
44
|
-
function scopeFor(record) {
|
|
45
|
-
return {
|
|
46
|
-
/** Register a revertible effect. The returned disposer runs on deactivation, LIFO. */
|
|
47
|
-
effect(fn) {
|
|
48
|
-
const dispose = fn();
|
|
49
|
-
if (typeof dispose === 'function') record.disposers.push(dispose);
|
|
50
|
-
return () => {
|
|
51
|
-
const i = record.disposers.indexOf(dispose);
|
|
52
|
-
if (i >= 0) { record.disposers.splice(i, 1); dispose(); }
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
/** Provide a capability. This is itself an effect, so it unwinds with the component. */
|
|
57
|
-
provide(key, value) {
|
|
58
|
-
if (bindings.has(key)) throw new Error(`registry: '${key}' is already provided`);
|
|
59
|
-
bindings.set(key, { value, providerId: record.id });
|
|
60
|
-
record.provided.add(key);
|
|
61
|
-
emit('provided', { key, name: record.name });
|
|
62
|
-
record.disposers.push(() => withdrawSync(key));
|
|
63
|
-
queueSettle();
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
/** Read a required capability. */
|
|
67
|
-
get(key) {
|
|
68
|
-
const b = bindings.get(key);
|
|
69
|
-
return b ? b.value : undefined;
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
/** A nested component. Disposes with its parent, because that is an effect too. */
|
|
73
|
-
register(child) {
|
|
74
|
-
const handle = register(child);
|
|
75
|
-
record.disposers.push(() => handle.dispose());
|
|
76
|
-
return handle;
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
get name() { return record.name; },
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function satisfied(record) {
|
|
84
|
-
return record.requires.every((k) => bindings.has(k));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function activate(record) {
|
|
88
|
-
record.state = STATE.ACTIVE;
|
|
89
|
-
try {
|
|
90
|
-
record.apply(scopeFor(record));
|
|
91
|
-
} catch (err) {
|
|
92
|
-
// A failure is recorded on the component and never propagated to its siblings —
|
|
93
|
-
// one broken component must not take the system down. Whatever it managed to
|
|
94
|
-
// register still unwinds.
|
|
95
|
-
record.state = STATE.INACTIVE;
|
|
96
|
-
record.error = err;
|
|
97
|
-
runDisposers(record);
|
|
98
|
-
emit('failed', { name: record.name, error: err });
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
emit('activated', { name: record.name });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function runDisposers(record) {
|
|
105
|
-
const results = [];
|
|
106
|
-
// LIFO — rule 1.
|
|
107
|
-
while (record.disposers.length > 0) {
|
|
108
|
-
const dispose = record.disposers.pop();
|
|
109
|
-
try { results.push(dispose()); } catch { /* one bad disposer must not strand the rest */ }
|
|
110
|
-
}
|
|
111
|
-
record.provided.clear();
|
|
112
|
-
return results.filter((r) => r && typeof r.then === 'function');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function deactivate(record) {
|
|
116
|
-
if (record.state !== STATE.ACTIVE) return [];
|
|
117
|
-
record.state = STATE.INACTIVE;
|
|
118
|
-
const pending = runDisposers(record);
|
|
119
|
-
emit('deactivated', { name: record.name });
|
|
120
|
-
return pending;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Rule 2: every ACTIVE dependent stands down BEFORE the binding disappears, so its
|
|
125
|
-
* teardown can still read the capability it is being torn down over.
|
|
126
|
-
*/
|
|
127
|
-
function withdrawSync(key) {
|
|
128
|
-
const binding = bindings.get(key);
|
|
129
|
-
if (!binding) return;
|
|
130
|
-
// Rule 2 creates a window: dependents are stood down while the binding they need is
|
|
131
|
-
// still present, precisely so their teardown can use it. Anything that settles inside
|
|
132
|
-
// that window sees an inactive component whose requirement is still satisfied and
|
|
133
|
-
// dutifully re-activates it — the component arms, then disarms again a moment later.
|
|
134
|
-
//
|
|
135
|
-
// That happens as soon as a second dependent exists, because tearing IT down withdraws
|
|
136
|
-
// what IT provided, and a nested withdrawal settles. With one dependent the window was
|
|
137
|
-
// never entered, which is why this survived until a second one was added.
|
|
138
|
-
//
|
|
139
|
-
// So the window is closed rather than the re-activation being detected afterwards:
|
|
140
|
-
// settling is suspended until the binding is actually gone.
|
|
141
|
-
const outermost = !withdrawing;
|
|
142
|
-
withdrawing = true;
|
|
143
|
-
try {
|
|
144
|
-
for (const record of components.values()) {
|
|
145
|
-
if (record.state === STATE.ACTIVE && record.requires.includes(key)) deactivate(record);
|
|
146
|
-
}
|
|
147
|
-
bindings.delete(key);
|
|
148
|
-
emit('withdrawn', { key });
|
|
149
|
-
} finally {
|
|
150
|
-
if (outermost) withdrawing = false;
|
|
151
|
-
}
|
|
152
|
-
queueSettle();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Fixpoint: activating one component can satisfy another, and so on. */
|
|
156
|
-
function settle() {
|
|
157
|
-
if (settling || disposed) return;
|
|
158
|
-
settling = true;
|
|
159
|
-
try {
|
|
160
|
-
for (let pass = 0; pass < 64; pass++) {
|
|
161
|
-
let moved = false;
|
|
162
|
-
for (const record of components.values()) {
|
|
163
|
-
if (record.error) continue; // failed stays failed
|
|
164
|
-
const ok = satisfied(record);
|
|
165
|
-
if (ok && record.state === STATE.INACTIVE) { activate(record); moved = true; }
|
|
166
|
-
else if (!ok && record.state === STATE.ACTIVE) { deactivate(record); moved = true; }
|
|
167
|
-
}
|
|
168
|
-
if (!moved) return;
|
|
169
|
-
}
|
|
170
|
-
throw new Error('registry: settle did not converge in 64 passes');
|
|
171
|
-
} finally {
|
|
172
|
-
settling = false;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function queueSettle() { if (!settling && !withdrawing) settle(); }
|
|
177
|
-
|
|
178
|
-
function register({ name, requires = [], apply }) {
|
|
179
|
-
if (typeof apply !== 'function') throw new TypeError('registry: component.apply required');
|
|
180
|
-
const record = {
|
|
181
|
-
id: `c${nextId++}`, name: name || 'anonymous', requires: [...requires], apply,
|
|
182
|
-
state: STATE.INACTIVE, disposers: [], provided: new Set(), error: null,
|
|
183
|
-
};
|
|
184
|
-
components.set(record.id, record);
|
|
185
|
-
queueSettle();
|
|
186
|
-
return {
|
|
187
|
-
get name() { return record.name; },
|
|
188
|
-
get state() { return record.state; },
|
|
189
|
-
get error() { return record.error; },
|
|
190
|
-
async dispose() {
|
|
191
|
-
const pending = deactivate(record);
|
|
192
|
-
components.delete(record.id);
|
|
193
|
-
await Promise.all(pending);
|
|
194
|
-
queueSettle();
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
register,
|
|
201
|
-
|
|
202
|
-
/** Provide from outside any component — the root of the graph. */
|
|
203
|
-
provide(key, value) {
|
|
204
|
-
if (bindings.has(key)) throw new Error(`registry: '${key}' is already provided`);
|
|
205
|
-
bindings.set(key, { value, providerId: null });
|
|
206
|
-
emit('provided', { key });
|
|
207
|
-
queueSettle();
|
|
208
|
-
return () => withdrawSync(key);
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
get: (key) => (bindings.has(key) ? bindings.get(key).value : undefined),
|
|
212
|
-
has: (key) => bindings.has(key),
|
|
213
|
-
keys: () => [...bindings.keys()].sort(),
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* What is waiting, and on what. A dependency cycle simply leaves its components
|
|
217
|
-
* permanently inactive — unlike a deadlock that depends on the schedule, this is
|
|
218
|
-
* visible from the declarations alone, so a host can report it at load time.
|
|
219
|
-
*/
|
|
220
|
-
pending() {
|
|
221
|
-
return [...components.values()]
|
|
222
|
-
.filter((r) => r.state === STATE.INACTIVE && !r.error)
|
|
223
|
-
.map((r) => ({ name: r.name, waitingFor: r.requires.filter((k) => !bindings.has(k)) }));
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
active: () => [...components.values()].filter((r) => r.state === STATE.ACTIVE).map((r) => r.name).sort(),
|
|
227
|
-
|
|
228
|
-
async dispose() {
|
|
229
|
-
disposed = true;
|
|
230
|
-
// Reverse registration order — the whole registry is itself one LIFO stack.
|
|
231
|
-
const all = [...components.values()].reverse();
|
|
232
|
-
const pending = all.flatMap((r) => deactivate(r));
|
|
233
|
-
components.clear();
|
|
234
|
-
bindings.clear();
|
|
235
|
-
await Promise.all(pending);
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export const REGISTRY_STATES = STATE;
|
|
1
|
+
const i=Object.freeze({INACTIVE:"inactive",ACTIVE:"active"});let A=0;function y({onEvent:p=null}={}){const r=new Map,a=new Map;let l=!1,c=!1,f=!1;const o=(e,t)=>{p&&p({event:e,...t})};function h(e){return{effect(t){const s=t();return typeof s=="function"&&e.disposers.push(s),()=>{const n=e.disposers.indexOf(s);n>=0&&(e.disposers.splice(n,1),s())}},provide(t,s){if(r.has(t))throw new Error(`registry: '${t}' is already provided`);r.set(t,{value:s,providerId:e.id}),e.provided.add(t),o("provided",{key:t,name:e.name}),e.disposers.push(()=>v(t)),u()},get(t){const s=r.get(t);return s?s.value:void 0},register(t){const s=m(t);return e.disposers.push(()=>s.dispose()),s},get name(){return e.name}}}function I(e){return e.requires.every(t=>r.has(t))}function w(e){e.state=i.ACTIVE;try{e.apply(h(e))}catch(t){e.state=i.INACTIVE,e.error=t,g(e),o("failed",{name:e.name,error:t});return}o("activated",{name:e.name})}function g(e){const t=[];for(;e.disposers.length>0;){const s=e.disposers.pop();try{t.push(s())}catch{}}return e.provided.clear(),t.filter(s=>s&&typeof s.then=="function")}function d(e){if(e.state!==i.ACTIVE)return[];e.state=i.INACTIVE;const t=g(e);return o("deactivated",{name:e.name}),t}function v(e){if(!r.get(e))return;const s=!f;f=!0;try{for(const n of a.values())n.state===i.ACTIVE&&n.requires.includes(e)&&d(n);r.delete(e),o("withdrawn",{key:e})}finally{s&&(f=!1)}u()}function E(){if(!(l||c)){l=!0;try{for(let e=0;e<64;e++){let t=!1;for(const s of a.values()){if(s.error)continue;const n=I(s);n&&s.state===i.INACTIVE?(w(s),t=!0):!n&&s.state===i.ACTIVE&&(d(s),t=!0)}if(!t)return}throw new Error("registry: settle did not converge in 64 passes")}finally{l=!1}}}function u(){!l&&!f&&E()}function m({name:e,requires:t=[],apply:s}){if(typeof s!="function")throw new TypeError("registry: component.apply required");const n={id:`c${A++}`,name:e||"anonymous",requires:[...t],apply:s,state:i.INACTIVE,disposers:[],provided:new Set,error:null};return a.set(n.id,n),u(),{get name(){return n.name},get state(){return n.state},get error(){return n.error},async dispose(){const T=d(n);a.delete(n.id),await Promise.all(T),u()}}}return{register:m,provide(e,t){if(r.has(e))throw new Error(`registry: '${e}' is already provided`);return r.set(e,{value:t,providerId:null}),o("provided",{key:e}),u(),()=>v(e)},get:e=>r.has(e)?r.get(e).value:void 0,has:e=>r.has(e),keys:()=>[...r.keys()].sort(),pending(){return[...a.values()].filter(e=>e.state===i.INACTIVE&&!e.error).map(e=>({name:e.name,waitingFor:e.requires.filter(t=>!r.has(t))}))},active:()=>[...a.values()].filter(e=>e.state===i.ACTIVE).map(e=>e.name).sort(),async dispose(){c=!0;const t=[...a.values()].reverse().flatMap(s=>d(s));a.clear(),r.clear(),await Promise.all(t)}}}const C=i;export{C as REGISTRY_STATES,y as createRegistry};
|
package/route-graph.js
CHANGED
|
@@ -1,115 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// A route currently explains itself in three lines of prose — which model, and two reasons.
|
|
4
|
-
// That is enough to read one decision and not nearly enough to find a wrong one: it cannot
|
|
5
|
-
// say what the alternatives were, how close they came, what was eliminated and why, or where
|
|
6
|
-
// the turn would go next if this model declined. Those are exactly the questions asked when
|
|
7
|
-
// the router picks something surprising, and answering them meant reading the code.
|
|
8
|
-
//
|
|
9
|
-
// So this derives the whole picture from one decision: every candidate with the numbers that
|
|
10
|
-
// decided it, and the failover chain the turn WOULD walk. It is a derivation, not a renderer
|
|
11
|
-
// — no DOM, no colours, no layout — so the side panel, the settings viewer, a desktop app or
|
|
12
|
-
// a CLI can each draw the same graph their own way.
|
|
13
|
-
//
|
|
14
|
-
// THE CHAIN IS COMPUTED BY THE SAME CODE THAT WILL WALK IT (failoverOrder). A projection with
|
|
15
|
-
// its own copy of the ordering would drift from the real thing, and a picture that lies about
|
|
16
|
-
// what the router is going to do is worse than no picture at all.
|
|
17
|
-
//
|
|
18
|
-
// Class R: arithmetic over a decision that has already been made. No model call, no I/O.
|
|
19
|
-
|
|
20
|
-
import { failoverOrder } from './router.js';
|
|
21
|
-
|
|
22
|
-
const num = (v) => (Number.isFinite(v) ? v : null);
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param decision the object returned by route()/routeWith().
|
|
26
|
-
* @param models every candidate considered, eligible or not (the router's full model list).
|
|
27
|
-
* @param hops how far to project the failover chain. The default matches what a user can
|
|
28
|
-
* actually sit through rather than the attempt cap.
|
|
29
|
-
* @returns { chosen, strategy, reasons, nodes, chain, eliminated }
|
|
30
|
-
* nodes — every candidate, ranked, with why it was eliminated when it was.
|
|
31
|
-
* chain — [chosen, ...replacements], the walk each subsequent decline would take.
|
|
32
|
-
*/
|
|
33
|
-
export function routeGraph({ decision = null, models = [], hops = 4 } = {}) {
|
|
34
|
-
if (!decision) return { chosen: null, strategy: null, reasons: [], constraints: [], nodes: [], chain: [], eliminated: 0 };
|
|
35
|
-
|
|
36
|
-
const eligible = decision.eligible || [];
|
|
37
|
-
const eligibleIds = new Set(eligible.map((m) => m.id));
|
|
38
|
-
const why = new Map((decision.rejected || []).map((r) => [r.id, r.why]));
|
|
39
|
-
// Rank is position in the router's own ordering — the thing that decided — so a node's
|
|
40
|
-
// place in the picture is the place it had in the decision, not a re-sort of our own.
|
|
41
|
-
const rankOf = new Map(eligible.map((m, i) => [m.id, i]));
|
|
42
|
-
|
|
43
|
-
const nodes = [...models]
|
|
44
|
-
.map((m) => ({
|
|
45
|
-
id: m.id,
|
|
46
|
-
label: m.label || m.id,
|
|
47
|
-
reach: m.reach,
|
|
48
|
-
classUsed: m.classUsed,
|
|
49
|
-
quality: num(m.quality),
|
|
50
|
-
costPer1k: num(m.costPer1k),
|
|
51
|
-
latencyMs: num(m.latencyMs),
|
|
52
|
-
// An order the user PINNED is a statement; one we inferred is a guess, and a picture
|
|
53
|
-
// that shows them identically hides why a model won.
|
|
54
|
-
order: num(m.providerRank),
|
|
55
|
-
orderPinned: !!m.orderPinned,
|
|
56
|
-
eligible: eligibleIds.has(m.id),
|
|
57
|
-
rank: rankOf.has(m.id) ? rankOf.get(m.id) : null,
|
|
58
|
-
chosen: m.id === decision.model?.id,
|
|
59
|
-
// Present only when it was ruled out — the whole point of showing the losers.
|
|
60
|
-
why: eligibleIds.has(m.id) ? null : (why.get(m.id) || 'not eligible'),
|
|
61
|
-
}))
|
|
62
|
-
.sort((a, b) => {
|
|
63
|
-
if (a.eligible !== b.eligible) return a.eligible ? -1 : 1;
|
|
64
|
-
if (a.eligible) return (a.rank ?? 0) - (b.rank ?? 0);
|
|
65
|
-
return String(a.label).localeCompare(String(b.label));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
chosen: decision.model?.id || null,
|
|
70
|
-
strategy: decision.strategy || null,
|
|
71
|
-
reasons: decision.reasons || [],
|
|
72
|
-
// WHY the constraints are what they are — which page capped the reach, what set the
|
|
73
|
-
// quality floor. "reach 'trusted' within 'trusted'" says a ceiling applied and not what
|
|
74
|
-
// imposed it, and an unexplained restriction is one people switch off wholesale.
|
|
75
|
-
constraints: decision.constraints || [],
|
|
76
|
-
nodes,
|
|
77
|
-
chain: projectChain(decision, hops),
|
|
78
|
-
eliminated: nodes.filter((n) => !n.eligible).length,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Where the turn goes if this model declines, and the one after that.
|
|
84
|
-
*
|
|
85
|
-
* Walked hop by hop rather than read off the ranking, because that is what actually happens:
|
|
86
|
-
* each replacement is chosen relative to the model that JUST failed, not to the original. A
|
|
87
|
-
* chain read off `eligible` order would be a plausible-looking fiction — it is the ordering
|
|
88
|
-
* for the first choice, not for the fourth.
|
|
89
|
-
*/
|
|
90
|
-
export function projectChain(decision, hops = 4) {
|
|
91
|
-
const first = decision?.model;
|
|
92
|
-
if (!first) return [];
|
|
93
|
-
const chain = [{ id: first.id, label: first.label || first.id, reason: null }];
|
|
94
|
-
const tried = new Set([first.id]);
|
|
95
|
-
let current = first;
|
|
96
|
-
|
|
97
|
-
for (let i = 0; i < hops; i++) {
|
|
98
|
-
const rest = (decision.eligible || []).filter((m) => !tried.has(m.id));
|
|
99
|
-
if (!rest.length) break;
|
|
100
|
-
// 'server' — a provider saying no, which is the ordinary case. A retired model reorders
|
|
101
|
-
// this, but that is a fact discovered at failure time and cannot be known in advance.
|
|
102
|
-
const next = failoverOrder(rest, {
|
|
103
|
-
model: current.model,
|
|
104
|
-
quality: current.quality,
|
|
105
|
-
capabilities: current.capabilities,
|
|
106
|
-
classUsed: current.classUsed,
|
|
107
|
-
reason: 'server',
|
|
108
|
-
})[0];
|
|
109
|
-
if (!next) break;
|
|
110
|
-
chain.push({ id: next.id, label: next.label || next.id, reason: `closest to ${current.label || current.id}` });
|
|
111
|
-
tried.add(next.id);
|
|
112
|
-
current = next;
|
|
113
|
-
}
|
|
114
|
-
return chain;
|
|
115
|
-
}
|
|
1
|
+
import{failoverOrder as u}from"./router.js";const d=l=>Number.isFinite(l)?l:null;function h({decision:l=null,models:c=[],hops:t=4}={}){if(!l)return{chosen:null,strategy:null,reasons:[],constraints:[],nodes:[],chain:[],eliminated:0};const r=l.eligible||[],a=new Set(r.map(e=>e.id)),i=new Map((l.rejected||[]).map(e=>[e.id,e.why])),s=new Map(r.map((e,n)=>[e.id,n])),o=[...c].map(e=>({id:e.id,label:e.label||e.id,reach:e.reach,classUsed:e.classUsed,quality:d(e.quality),costPer1k:d(e.costPer1k),latencyMs:d(e.latencyMs),order:d(e.providerRank),orderPinned:!!e.orderPinned,eligible:a.has(e.id),rank:s.has(e.id)?s.get(e.id):null,chosen:e.id===l.model?.id,why:a.has(e.id)?null:i.get(e.id)||"not eligible"})).sort((e,n)=>e.eligible!==n.eligible?e.eligible?-1:1:e.eligible?(e.rank??0)-(n.rank??0):String(e.label).localeCompare(String(n.label)));return{chosen:l.model?.id||null,strategy:l.strategy||null,reasons:l.reasons||[],constraints:l.constraints||[],nodes:o,chain:b(l,t),eliminated:o.filter(e=>!e.eligible).length}}function b(l,c=4){const t=l?.model;if(!t)return[];const r=[{id:t.id,label:t.label||t.id,reason:null}],a=new Set([t.id]);let i=t;for(let s=0;s<c;s++){const o=(l.eligible||[]).filter(n=>!a.has(n.id));if(!o.length)break;const e=u(o,{model:i.model,quality:i.quality,capabilities:i.capabilities,classUsed:i.classUsed,reason:"server"})[0];if(!e)break;r.push({id:e.id,label:e.label||e.id,reason:`closest to ${i.label||i.id}`}),a.add(e.id),i=e}return r}export{b as projectChain,h as routeGraph};
|
package/route-strategies.js
CHANGED
|
@@ -1,232 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// The routing CONTRACT (createModelRouter, signals, requirements, failover order) has lived
|
|
4
|
-
// in this package since it was written; the DECISIONS layered on it — escalate hard work,
|
|
5
|
-
// replace like with like, honour "use claude" — were typed in the extension, so the desktop
|
|
6
|
-
// was about to copy them. They are pure functions of the candidates and the need, so they
|
|
7
|
-
// belong here, and both clients build the same router from the same declarations.
|
|
8
|
-
//
|
|
9
|
-
// `needForTurn` builds the need from the turn's facts. The source guard (which page or
|
|
10
|
-
// record the turn is about, and what that caps reach at) is INJECTED as `guard`, because
|
|
11
|
-
// reading it means reading a client's settings; the ceiling itself is enforced here.
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
defineMiddleware, defineRouteStrategy, signalsFrom, requirementsFor, preferenceFor,
|
|
15
|
-
failoverOrder, pinnedOrderOf,
|
|
16
|
-
} from './router.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Redaction is REQUIRED for anything that leaves the user's machine.
|
|
20
|
-
*
|
|
21
|
-
* Declared as middleware with `requiredFor` so the router refuses to route to a third party
|
|
22
|
-
* when it is not active. That is the difference between "we always redact" as a habit and as
|
|
23
|
-
* a property: a disabled plugin, a refactor or a new caller cannot quietly skip it.
|
|
24
|
-
*/
|
|
25
|
-
export const redactionStep = defineMiddleware({
|
|
26
|
-
id: 'redaction',
|
|
27
|
-
label: 'Redaction',
|
|
28
|
-
stage: 'request',
|
|
29
|
-
priority: 10, // before anything that reads the text
|
|
30
|
-
requiredFor: (model) => model.reach === 'any',
|
|
31
|
-
// The actual redaction still happens in streamChat's harness. This declares the
|
|
32
|
-
// REQUIREMENT; wiring the implementation through here is the next step, and doing both at
|
|
33
|
-
// once would mean changing what redaction does in the same commit that changes when it runs.
|
|
34
|
-
run: async (request) => request,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Escalate when the task is actually hard.
|
|
39
|
-
*
|
|
40
|
-
* The router was picking the cheapest eligible model for everything, which is right for
|
|
41
|
-
* "hello" and wrong for "draw a circle around Mickey" — a request needing spatial reasoning
|
|
42
|
-
* and a structured payload went to a 26B model because it was free. Cost is the correct
|
|
43
|
-
* tie-breaker among models that can all do the job; it is the wrong one when they cannot.
|
|
44
|
-
*
|
|
45
|
-
* Class R: length, code fences, image content and page tools are all readable for nothing.
|
|
46
|
-
* The escalation itself costs no model call — only the answer does, and that is the point.
|
|
47
|
-
*/
|
|
48
|
-
export const complexityStrategy = defineRouteStrategy({
|
|
49
|
-
id: 'escalate-on-complexity',
|
|
50
|
-
label: 'Escalate hard tasks',
|
|
51
|
-
classUsed: 'R',
|
|
52
|
-
decide: async (eligible, need) => {
|
|
53
|
-
const sig = need.signals;
|
|
54
|
-
// ASKED FOR NOTHING, ESCALATES TO NOTHING. This fired on 'hello' because the caller was
|
|
55
|
-
// passing `structured: structured || pageTools`, so every turn on a page with actions
|
|
56
|
-
// armed looked like exact structured work. Equipment is not demand — the same conflation
|
|
57
|
-
// that put a quality floor on a greeting, in a second place.
|
|
58
|
-
if (sig?.smalltalk) return null;
|
|
59
|
-
// NOR DOES BACKGROUND WORK ESCALATE. Dropping the quality floor for a topic pass and then
|
|
60
|
-
// letting escalation rank by quality anyway would move the same decision one step down
|
|
61
|
-
// and change nothing — the floor eliminated the local models, this would simply rank them
|
|
62
|
-
// last. Both read 'high' from the size of the material rather than the difficulty of the
|
|
63
|
-
// ask, so both have to abstain.
|
|
64
|
-
if (need.background) return null;
|
|
65
|
-
const hard = sig?.complexity === 'high' || sig?.modality === 'vision' || need.structured;
|
|
66
|
-
if (!hard) return null; // no opinion on easy work — let cost decide
|
|
67
|
-
// Prefer a model that claims what this task actually wants. Not a hard filter: declaring
|
|
68
|
-
// "reasoning" required would eliminate every model on a setup where nobody has ticked
|
|
69
|
-
// the box, and an empty candidate list is a worse answer than a merely adequate model.
|
|
70
|
-
const wants = new Set();
|
|
71
|
-
if (sig?.complexity === 'high') wants.add('reasoning');
|
|
72
|
-
if (need.structured) wants.add('tools');
|
|
73
|
-
if (sig?.modality === 'vision') wants.add('vision');
|
|
74
|
-
if (sig?.approxTokens > 20_000) wants.add('long-context');
|
|
75
|
-
const fit = (m) => [...wants].filter((c) => m.capabilities.includes(c)).length;
|
|
76
|
-
const best = Math.max(...eligible.map(fit));
|
|
77
|
-
let shortlist = best > 0 ? eligible.filter((m) => fit(m) === best) : eligible;
|
|
78
|
-
|
|
79
|
-
// STRUCTURED WORK WANTS A MODEL, NOT AN AGENT.
|
|
80
|
-
//
|
|
81
|
-
// A canvas or spreadsheet adapter is one call: hand it the data, it applies it, done. A
|
|
82
|
-
// CLI agent runs its OWN loop — it explores, reads files, decides what to do next — and
|
|
83
|
-
// having applied the shapes correctly it carries on, because finishing is not something
|
|
84
|
-
// its loop is told about. A user watched the circle appear and then waited until they
|
|
85
|
-
// killed the process.
|
|
86
|
-
//
|
|
87
|
-
// Not a hard filter: on a setup with only agents, an agent that overruns still beats no
|
|
88
|
-
// answer.
|
|
89
|
-
if (need.structured) {
|
|
90
|
-
const models = shortlist.filter((m) => m.classUsed !== 'A');
|
|
91
|
-
if (models.length) shortlist = models;
|
|
92
|
-
}
|
|
93
|
-
// Rank by declared quality — the axis this strategy exists to judge — then by the ORDER
|
|
94
|
-
// the user set, and only then by cost. A model with an unknown quality sits mid-table
|
|
95
|
-
// rather than last, so a newly added model is not permanently skipped.
|
|
96
|
-
//
|
|
97
|
-
// ORDER BEFORE COST, and this is the fix for a real complaint: three CLI agents of
|
|
98
|
-
// identical quality, one of them pinned to Order 1, and escalation picked a different one
|
|
99
|
-
// because it is cheaper per 1k. A hand-set order is a statement — they can see the prices
|
|
100
|
-
// and chose anyway — and it was being honoured in the score path and nowhere else, so
|
|
101
|
-
// the moment any strategy had an opinion the user's own preference stopped existing.
|
|
102
|
-
//
|
|
103
|
-
// Still only a TIE-BREAK: quality decides first, so a genuinely better model beats the
|
|
104
|
-
// pinned one, and an INFERRED order stays below cost where a guess belongs.
|
|
105
|
-
const q = (m) => (Number.isFinite(m.quality) ? m.quality : 0.5);
|
|
106
|
-
return [...shortlist].sort((a, b) => q(b) - q(a)
|
|
107
|
-
|| pinnedOrderOf(a) - pinnedOrderOf(b) || a.costPer1k - b.costPer1k);
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* When a model declines, replace it with the closest thing available — not the cheapest.
|
|
113
|
-
*
|
|
114
|
-
* A frontier model that ran out of credits mid-task should be replaced by the same model at
|
|
115
|
-
* another provider, or by something comparably capable. Falling back to a small local model
|
|
116
|
-
* is how a drawing that was going well turns into a circle in the wrong place: the task did
|
|
117
|
-
* not get easier when the provider said no.
|
|
118
|
-
*
|
|
119
|
-
* Ranked by closeness to what failed, in the order that actually matters:
|
|
120
|
-
* 1. the SAME model somewhere else — identical capability, merely a different bill;
|
|
121
|
-
* 2. a model with every capability the failed one had, best quality first;
|
|
122
|
-
* 3. anything else, so the turn still completes rather than dying.
|
|
123
|
-
*/
|
|
124
|
-
export const failoverStrategy = defineRouteStrategy({
|
|
125
|
-
id: 'failover-to-similar',
|
|
126
|
-
label: 'Replace like with like',
|
|
127
|
-
classUsed: 'R',
|
|
128
|
-
decide: async (eligible, need) => {
|
|
129
|
-
const failed = need.like;
|
|
130
|
-
if (!failed) return null;
|
|
131
|
-
// THE ORDERING ITSELF LIVES IN @chatpanel/events, because two things need it: this
|
|
132
|
-
// strategy, and the projected chain the trace draws before any of it happens. A picture
|
|
133
|
-
// computed by a second implementation would eventually disagree with the real failover,
|
|
134
|
-
// and one that lies about what the router will do is worse than no picture. The strategy
|
|
135
|
-
// is the thin part — knowing there IS something to replace.
|
|
136
|
-
return failoverOrder(eligible, failed);
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* "use claude" is an instruction, not a topic.
|
|
142
|
-
*
|
|
143
|
-
* A user naming a model in their message was being ignored entirely — the router read
|
|
144
|
-
* length, modality and tools, and not the one signal that is an explicit answer to the
|
|
145
|
-
* question it was asking. Asking for a specific model and being given another is the most
|
|
146
|
-
* annoying possible failure of a router, because it looks like the request was not read.
|
|
147
|
-
*
|
|
148
|
-
* DELIBERATELY CONSERVATIVE. Only imperative forms count — "use X", "with X", "ask X",
|
|
149
|
-
* "switch to X" — so "tell me about claude" stays a question about Claude rather than a
|
|
150
|
-
* routing instruction. A false positive here silently sends work to the wrong model, which
|
|
151
|
-
* is worse than missing an unusual phrasing.
|
|
152
|
-
*
|
|
153
|
-
* It still cannot widen reach: like every strategy it only ever chooses among candidates the
|
|
154
|
-
* hard constraints already allowed. A device-only request naming a cloud model still stays
|
|
155
|
-
* on-device.
|
|
156
|
-
*/
|
|
157
|
-
export const explicitModelStrategy = defineRouteStrategy({
|
|
158
|
-
id: 'named-by-user',
|
|
159
|
-
label: 'Use the model you asked for',
|
|
160
|
-
classUsed: 'R',
|
|
161
|
-
decide: async (eligible, need) => {
|
|
162
|
-
const text = String(need.requestText || '').toLowerCase();
|
|
163
|
-
if (!text) return null;
|
|
164
|
-
const directive = /\b(?:use|using|with|via|ask|switch to|route to|try)\s+([a-z0-9][a-z0-9.\- ]{1,28})/g;
|
|
165
|
-
const asked = [];
|
|
166
|
-
for (const m of text.matchAll(directive)) asked.push(m[1].trim());
|
|
167
|
-
if (!asked.length) return null;
|
|
168
|
-
|
|
169
|
-
const matches = eligible.filter((cand) => {
|
|
170
|
-
const names = [cand.label, cand.model, cand.id].filter(Boolean).map((x) => String(x).toLowerCase());
|
|
171
|
-
return asked.some((want) => names.some((n) => n.includes(want) || want.includes(n.split(' · ')[0])));
|
|
172
|
-
});
|
|
173
|
-
if (!matches.length) return null; // named something we do not have? say nothing and let the rest decide
|
|
174
|
-
// WHICH ONE, when the name matches several. This took the FIRST match in score order, so
|
|
175
|
-
// "use claude" on a setup with three Claude routes picked whichever happened to score
|
|
176
|
-
// best — a cost-and-latency guess deciding a question the user had already answered
|
|
177
|
-
// twice: once by naming the model, and once by ordering the routes to it.
|
|
178
|
-
//
|
|
179
|
-
// Returned as a LIST rather than a single model, so the ones that also matched become the
|
|
180
|
-
// runners-up: if the first declines, failover replaces it with another route to the model
|
|
181
|
-
// that was actually asked for.
|
|
182
|
-
return [...matches].sort((a, b) => pinnedOrderOf(a) - pinnedOrderOf(b));
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Ordered deliberately: an explicit request outranks every heuristic, because the user has
|
|
187
|
-
// answered the question the router was about to guess at. Failover next — a decline is newer
|
|
188
|
-
// information than the preference that made the original choice. Escalation is the general
|
|
189
|
-
// case.
|
|
190
|
-
export const ROUTE_STRATEGIES = [explicitModelStrategy, failoverStrategy, complexityStrategy];
|
|
191
|
-
export const ROUTE_MIDDLEWARE = [redactionStep];
|
|
192
|
-
|
|
193
|
-
export function needForTurn({ capabilities = [], request = null, structured = false, pageTools = false, force = false, background = false, guard = null } = {}) {
|
|
194
|
-
const signals = request ? signalsFrom(request) : {};
|
|
195
|
-
// REQUIREMENTS FIRST. What the work needs eliminates candidates; cost and speed only order
|
|
196
|
-
// what survives. A preference lets an unsuitable model win once the better ones decline,
|
|
197
|
-
// which is exactly how a chain of five ended on one that could not do the job.
|
|
198
|
-
const req = requirementsFor(signals, { structured, pageTools, hasTools: capabilities.includes('tools'), background });
|
|
199
|
-
// WHERE IT CAME FROM IS A CEILING, NOT A PREFERENCE. Routing asked what the work needed and
|
|
200
|
-
// never asked what it was about, so an internal page was summarised by a public inference
|
|
201
|
-
// host. This narrows reach and can only narrow it — reach is never relaxed (see the
|
|
202
|
-
// relaxation order in the router), so no later step can trade it away for capability.
|
|
203
|
-
// WHICH AXIS THIS REQUEST CARES ABOUT, read from the request rather than fixed at
|
|
204
|
-
// 'balanced' for everything. A greeting means fast — no answer to "hi" is improved by a
|
|
205
|
-
// frontier model thinking about it. A refactor means good — three seconds saved on an
|
|
206
|
-
// answer that has to be redone is not a saving. It only ever ORDERS what already
|
|
207
|
-
// qualifies; `req` above is what eliminates.
|
|
208
|
-
const pref = preferenceFor(signals, {
|
|
209
|
-
structured,
|
|
210
|
-
minQuality: req.minQuality,
|
|
211
|
-
// The same fact requirementsFor was given: a turn carrying tools is one that might use
|
|
212
|
-
// them, so it is never "answer fast at any quality".
|
|
213
|
-
hasTools: capabilities.includes('tools'),
|
|
214
|
-
background,
|
|
215
|
-
});
|
|
216
|
-
return {
|
|
217
|
-
prefer: pref.prefer,
|
|
218
|
-
reach: guard ? guard.reach : 'any',
|
|
219
|
-
capabilities: [...new Set([...capabilities, ...req.required])],
|
|
220
|
-
minQuality: req.minQuality,
|
|
221
|
-
// Which requirements may be given up if nothing qualifies — never `tools`, and never
|
|
222
|
-
// reach. See requirementsFor.
|
|
223
|
-
negotiable: req.negotiable,
|
|
224
|
-
requirementReasons: [...(guard ? [...req.why, guard.why] : req.why), pref.why],
|
|
225
|
-
sourceGuard: guard,
|
|
226
|
-
signals,
|
|
227
|
-
requestText: request ? String(request.text || (request.messages || []).map((m) => m?.content || '').join('\n')).slice(-2000) : '',
|
|
228
|
-
structured,
|
|
229
|
-
force,
|
|
230
|
-
background,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
1
|
+
import{defineMiddleware as m,defineRouteStrategy as f,signalsFrom as y,requirementsFor as h,preferenceFor as p,failoverOrder as x,pinnedOrderOf as u}from"./router.js";const g=m({id:"redaction",label:"Redaction",stage:"request",priority:10,requiredFor:i=>i.reach==="any",run:async i=>i}),w=f({id:"escalate-on-complexity",label:"Escalate hard tasks",classUsed:"R",decide:async(i,r)=>{const t=r.signals;if(t?.smalltalk||r.background||!(t?.complexity==="high"||t?.modality==="vision"||r.structured))return null;const n=new Set;t?.complexity==="high"&&n.add("reasoning"),r.structured&&n.add("tools"),t?.modality==="vision"&&n.add("vision"),t?.approxTokens>2e4&&n.add("long-context");const a=e=>[...n].filter(c=>e.capabilities.includes(c)).length,s=Math.max(...i.map(a));let l=s>0?i.filter(e=>a(e)===s):i;if(r.structured){const e=l.filter(c=>c.classUsed!=="A");e.length&&(l=e)}const o=e=>Number.isFinite(e.quality)?e.quality:.5;return[...l].sort((e,c)=>o(c)-o(e)||u(e)-u(c)||e.costPer1k-c.costPer1k)}}),S=f({id:"failover-to-similar",label:"Replace like with like",classUsed:"R",decide:async(i,r)=>{const t=r.like;return t?x(i,t):null}}),R=f({id:"named-by-user",label:"Use the model you asked for",classUsed:"R",decide:async(i,r)=>{const t=String(r.requestText||"").toLowerCase();if(!t)return null;const d=/\b(?:use|using|with|via|ask|switch to|route to|try)\s+([a-z0-9][a-z0-9.\- ]{1,28})/g,n=[];for(const s of t.matchAll(d))n.push(s[1].trim());if(!n.length)return null;const a=i.filter(s=>{const l=[s.label,s.model,s.id].filter(Boolean).map(o=>String(o).toLowerCase());return n.some(o=>l.some(e=>e.includes(o)||o.includes(e.split(" · ")[0])))});return a.length?[...a].sort((s,l)=>u(s)-u(l)):null}}),T=[R,S,w],v=[g];function q({capabilities:i=[],request:r=null,structured:t=!1,pageTools:d=!1,force:n=!1,background:a=!1,guard:s=null}={}){const l=r?y(r):{},o=h(l,{structured:t,pageTools:d,hasTools:i.includes("tools"),background:a}),e=p(l,{structured:t,minQuality:o.minQuality,hasTools:i.includes("tools"),background:a});return{prefer:e.prefer,reach:s?s.reach:"any",capabilities:[...new Set([...i,...o.required])],minQuality:o.minQuality,negotiable:o.negotiable,requirementReasons:[...s?[...o.why,s.why]:o.why,e.why],sourceGuard:s,signals:l,requestText:r?String(r.text||(r.messages||[]).map(c=>c?.content||"").join(`
|
|
2
|
+
`)).slice(-2e3):"",structured:t,force:n,background:a}}export{v as ROUTE_MIDDLEWARE,T as ROUTE_STRATEGIES,w as complexityStrategy,R as explicitModelStrategy,S as failoverStrategy,q as needForTurn,g as redactionStep};
|