@crewhaus/spec 0.2.0 → 0.2.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.
- package/dist/index.d.ts +5047 -125
- package/dist/index.js +148 -27
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -195,6 +195,13 @@ const toolConfigBlock = z
|
|
|
195
195
|
* normal model-router path — a fallback with a missing key warns at boot
|
|
196
196
|
* and is skipped when tried, never hard-failing the run.
|
|
197
197
|
*
|
|
198
|
+
* The declared ORDER is honoured verbatim — it is a TRUST ordering ("what do
|
|
199
|
+
* I run when my primary is down"), and the runtime never reorders it (a
|
|
200
|
+
* 2026-07 design review deliberately retired the cost-ranking idea; see the
|
|
201
|
+
* `rankFallbacks` docstring in model-router's failover.ts). Want cheaper
|
|
202
|
+
* models preferred? Order the list yourself — or use `agent.model_pool`,
|
|
203
|
+
* which is the cost/quality routing surface.
|
|
204
|
+
*
|
|
198
205
|
* `circuit_breaker` tunes the per-candidate breakers. Field names mirror
|
|
199
206
|
* `CircuitBreakerOptions` in `@crewhaus/circuit-breaker` exactly
|
|
200
207
|
* (failureThreshold / windowMs / cooldownMs); package defaults (5 failures
|
|
@@ -241,6 +248,110 @@ const modelTiersBlock = z
|
|
|
241
248
|
})
|
|
242
249
|
.strict()
|
|
243
250
|
.optional();
|
|
251
|
+
/**
|
|
252
|
+
* Adaptive model routing — an opt-in `model_pool` of user-declared candidate
|
|
253
|
+
* models the runtime selects among PER TURN, with a selection `policy` that can
|
|
254
|
+
* improve the more the harness runs. The N-candidate generalisation of
|
|
255
|
+
* `model_tiers` (which is the two-candidate special case), so the two are
|
|
256
|
+
* mutually exclusive on an agent block (enforced by a refine on each shape).
|
|
257
|
+
*
|
|
258
|
+
* The user always declares the SET; the runtime only ever picks WITHIN it —
|
|
259
|
+
* `agent.model` (and the optimizer's model-path exclusion) is untouched, so
|
|
260
|
+
* learning tunes selection policy, never the candidate roster.
|
|
261
|
+
*
|
|
262
|
+
* - `candidates` — ≥2 model-router grammar strings, each with free-form
|
|
263
|
+
* `tags` (e.g. `cheap`, `strong`) the heuristic routes on. Declare
|
|
264
|
+
* cheapest→strongest; tags override that order.
|
|
265
|
+
* - `policy` — `static` (first candidate), `heuristic` (deterministic
|
|
266
|
+
* difficulty routing, the default), or `learned` (reward-scoreboard arm
|
|
267
|
+
* selection that improves with usage).
|
|
268
|
+
* - `objective` — weights for the learned reward (quality / cost / latency);
|
|
269
|
+
* defaults to quality-dominant (0.7 / 0.2 / 0.1).
|
|
270
|
+
* - `routing` — difficulty thresholds (shared with `model_tiers`) plus the
|
|
271
|
+
* `strongTag`/`cheapTag` the heuristic prefers.
|
|
272
|
+
* - `learning` — read only for `policy: learned`: the per-arm exploration
|
|
273
|
+
* floor and the cost/latency reward references.
|
|
274
|
+
*
|
|
275
|
+
* Omitted entirely → single-model behaviour, byte-identical bundles.
|
|
276
|
+
*/
|
|
277
|
+
const modelPoolBlock = z
|
|
278
|
+
.object({
|
|
279
|
+
candidates: z
|
|
280
|
+
.array(z
|
|
281
|
+
.object({
|
|
282
|
+
model: z.string().min(1),
|
|
283
|
+
tags: z.array(z.string().min(1)).default([]),
|
|
284
|
+
})
|
|
285
|
+
.strict())
|
|
286
|
+
.min(2),
|
|
287
|
+
policy: z.enum(["static", "heuristic", "learned"]).default("heuristic"),
|
|
288
|
+
objective: z
|
|
289
|
+
.object({
|
|
290
|
+
quality: z.number().min(0).optional(),
|
|
291
|
+
cost: z.number().min(0).optional(),
|
|
292
|
+
latency: z.number().min(0).optional(),
|
|
293
|
+
})
|
|
294
|
+
.strict()
|
|
295
|
+
.optional(),
|
|
296
|
+
routing: z
|
|
297
|
+
.object({
|
|
298
|
+
contextTokenThreshold: z.number().int().positive().optional(),
|
|
299
|
+
toolsToDefault: z.boolean().optional(),
|
|
300
|
+
firstTurnToDefault: z.boolean().optional(),
|
|
301
|
+
priorToolDensityThreshold: z.number().int().positive().optional(),
|
|
302
|
+
strongTag: z.string().min(1).optional(),
|
|
303
|
+
cheapTag: z.string().min(1).optional(),
|
|
304
|
+
})
|
|
305
|
+
.strict()
|
|
306
|
+
.optional(),
|
|
307
|
+
learning: z
|
|
308
|
+
.object({
|
|
309
|
+
minSamplesPerArm: z.number().int().positive().optional(),
|
|
310
|
+
costRefUsd: z.number().positive().optional(),
|
|
311
|
+
latencyRefMs: z.number().int().positive().optional(),
|
|
312
|
+
// ε for ε-greedy online exploration once every arm clears the sample
|
|
313
|
+
// floor (fraction of exploit-phase turns that try a non-best model).
|
|
314
|
+
// Default 0 → deterministic explore-then-exploit, no RNG.
|
|
315
|
+
explorationRate: z.number().min(0).max(1).optional(),
|
|
316
|
+
// Fixed exploration seed for reproducible-across-runs behaviour (e.g.
|
|
317
|
+
// tests). Omitted → the runtime seeds from the sessionId, so each run
|
|
318
|
+
// explores differently while still replaying from its own transcript.
|
|
319
|
+
seed: z.string().min(1).optional(),
|
|
320
|
+
// Exploit-phase exploration strategy. "epsilon-greedy" (default) uses
|
|
321
|
+
// explorationRate; "thompson" draws each arm from its reward posterior
|
|
322
|
+
// and self-balances (explorationRate is then ignored).
|
|
323
|
+
bandit: z.enum(["epsilon-greedy", "thompson"]).optional(),
|
|
324
|
+
})
|
|
325
|
+
.strict()
|
|
326
|
+
.optional(),
|
|
327
|
+
})
|
|
328
|
+
.strict()
|
|
329
|
+
.optional();
|
|
330
|
+
/**
|
|
331
|
+
* Mutual-exclusion refine shared by every agent block that carries model
|
|
332
|
+
* routing: `model_pool` is the superset of both the two-tier router and the
|
|
333
|
+
* ordered failover chain, so declaring it alongside either is an error rather
|
|
334
|
+
* than an ambiguous double-route. (Per-candidate failover chains compose in a
|
|
335
|
+
* later release; this release keeps precedence unambiguous.)
|
|
336
|
+
*/
|
|
337
|
+
function refineModelSelection(agent, ctx) {
|
|
338
|
+
if (agent.model_pool === undefined)
|
|
339
|
+
return;
|
|
340
|
+
if (agent.model_tiers !== undefined) {
|
|
341
|
+
ctx.addIssue({
|
|
342
|
+
code: z.ZodIssueCode.custom,
|
|
343
|
+
message: "agent.model_pool and agent.model_tiers are mutually exclusive — model_pool is the N-candidate generalisation of the two-tier router",
|
|
344
|
+
path: ["model_tiers"],
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (agent.model_fallbacks !== undefined) {
|
|
348
|
+
ctx.addIssue({
|
|
349
|
+
code: z.ZodIssueCode.custom,
|
|
350
|
+
message: "agent.model_pool and agent.model_fallbacks are mutually exclusive in this release — per-candidate failover chains are a future addition",
|
|
351
|
+
path: ["model_fallbacks"],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
244
355
|
/**
|
|
245
356
|
* Section 17 — optional override for the model used by
|
|
246
357
|
* `compaction-autocompact` when summarising long conversations. Defaults
|
|
@@ -630,9 +741,12 @@ const cliSchema = z
|
|
|
630
741
|
circuit_breaker: circuitBreakerBlock,
|
|
631
742
|
// Item 26 — opt-in two-tier turn-difficulty router.
|
|
632
743
|
model_tiers: modelTiersBlock,
|
|
744
|
+
// Adaptive model routing — N-candidate pool with a selection policy.
|
|
745
|
+
model_pool: modelPoolBlock,
|
|
633
746
|
sub_agents: subAgentsBlock,
|
|
634
747
|
})
|
|
635
|
-
.strict()
|
|
748
|
+
.strict()
|
|
749
|
+
.superRefine(refineModelSelection),
|
|
636
750
|
tools: z.array(z.string().min(1)).optional(),
|
|
637
751
|
tool_config: toolConfigBlock,
|
|
638
752
|
mcp_servers: mcpServersBlock,
|
|
@@ -751,11 +865,14 @@ const channelAgentSchema = z
|
|
|
751
865
|
circuit_breaker: circuitBreakerBlock,
|
|
752
866
|
// Item 26 — opt-in two-tier turn-difficulty router.
|
|
753
867
|
model_tiers: modelTiersBlock,
|
|
868
|
+
// Adaptive model routing — N-candidate pool with a selection policy.
|
|
869
|
+
model_pool: modelPoolBlock,
|
|
754
870
|
tools: z.array(z.string().min(1)).optional(),
|
|
755
871
|
tool_config: toolConfigBlock,
|
|
756
872
|
sub_agents: subAgentsBlock,
|
|
757
873
|
})
|
|
758
|
-
.strict()
|
|
874
|
+
.strict()
|
|
875
|
+
.superRefine(refineModelSelection);
|
|
759
876
|
const channelSchema = z
|
|
760
877
|
.object({
|
|
761
878
|
name: safeName,
|
|
@@ -850,8 +967,11 @@ const managedAgentSchema = z
|
|
|
850
967
|
circuit_breaker: circuitBreakerBlock,
|
|
851
968
|
// Item 26 — opt-in two-tier turn-difficulty router.
|
|
852
969
|
model_tiers: modelTiersBlock,
|
|
970
|
+
// Adaptive model routing — N-candidate pool with a selection policy.
|
|
971
|
+
model_pool: modelPoolBlock,
|
|
853
972
|
})
|
|
854
|
-
.strict()
|
|
973
|
+
.strict()
|
|
974
|
+
.superRefine(refineModelSelection);
|
|
855
975
|
const managedSchema = z
|
|
856
976
|
.object({
|
|
857
977
|
name: safeName,
|
|
@@ -885,17 +1005,33 @@ const pipelineDocumentSchema = z
|
|
|
885
1005
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
886
1006
|
})
|
|
887
1007
|
.strict();
|
|
1008
|
+
/**
|
|
1009
|
+
* Adaptive model routing — the minimal single-agent block shared by the
|
|
1010
|
+
* pipeline/research/batch/browser shapes, now carrying the opt-in
|
|
1011
|
+
* `model_pool`. Their emitted runtimes each call `runChatLoop` with a single
|
|
1012
|
+
* primary (exactly the cli shape's execution model), so the pool routes there
|
|
1013
|
+
* with zero runtime changes. The superRefine is trivially satisfied today
|
|
1014
|
+
* (these shapes carry no `model_tiers`/`model_fallbacks`) but keeps the
|
|
1015
|
+
* mutual-exclusion rule uniform if they ever gain them. NOT used by
|
|
1016
|
+
* onchain/onchain-game: their emitted bundles are callable modules whose
|
|
1017
|
+
* agent-loop wiring is still deferred (see target-onchain slice-2 notes), so
|
|
1018
|
+
* a `model_pool` there would be an inert spec field.
|
|
1019
|
+
*/
|
|
1020
|
+
const pooledSingleAgentSchema = z
|
|
1021
|
+
.object({
|
|
1022
|
+
model: z.string().min(1),
|
|
1023
|
+
instructions: z.string().min(1),
|
|
1024
|
+
// Adaptive model routing — N-candidate pool with a selection policy.
|
|
1025
|
+
model_pool: modelPoolBlock,
|
|
1026
|
+
})
|
|
1027
|
+
.strict()
|
|
1028
|
+
.superRefine(refineModelSelection);
|
|
888
1029
|
const pipelineSchema = z
|
|
889
1030
|
.object({
|
|
890
1031
|
name: safeName,
|
|
891
1032
|
version: versionField,
|
|
892
1033
|
target: z.literal("pipeline"),
|
|
893
|
-
agent:
|
|
894
|
-
.object({
|
|
895
|
-
model: z.string().min(1),
|
|
896
|
-
instructions: z.string().min(1),
|
|
897
|
-
})
|
|
898
|
-
.strict(),
|
|
1034
|
+
agent: pooledSingleAgentSchema,
|
|
899
1035
|
retrieve: z
|
|
900
1036
|
.object({
|
|
901
1037
|
embedderModel: z.string().min(1),
|
|
@@ -991,12 +1127,7 @@ const researchSchema = z
|
|
|
991
1127
|
name: safeName,
|
|
992
1128
|
version: versionField,
|
|
993
1129
|
target: z.literal("research"),
|
|
994
|
-
agent:
|
|
995
|
-
.object({
|
|
996
|
-
model: z.string().min(1),
|
|
997
|
-
instructions: z.string().min(1),
|
|
998
|
-
})
|
|
999
|
-
.strict(),
|
|
1130
|
+
agent: pooledSingleAgentSchema,
|
|
1000
1131
|
goal: z.string().min(1),
|
|
1001
1132
|
branchingFactor: z.number().int().min(1).max(8).default(3),
|
|
1002
1133
|
maxDurationMs: z.number().int().positive().default(300_000),
|
|
@@ -1032,12 +1163,7 @@ const batchSchema = z
|
|
|
1032
1163
|
name: safeName,
|
|
1033
1164
|
version: versionField,
|
|
1034
1165
|
target: z.literal("batch"),
|
|
1035
|
-
agent:
|
|
1036
|
-
.object({
|
|
1037
|
-
model: z.string().min(1),
|
|
1038
|
-
instructions: z.string().min(1),
|
|
1039
|
-
})
|
|
1040
|
-
.strict(),
|
|
1166
|
+
agent: pooledSingleAgentSchema,
|
|
1041
1167
|
queue: batchQueueSchema,
|
|
1042
1168
|
concurrency: z.number().int().min(1).max(64).default(4),
|
|
1043
1169
|
idempotencyWindowMs: z.number().int().positive().default(60_000),
|
|
@@ -1108,12 +1234,7 @@ const browserSchema = z
|
|
|
1108
1234
|
name: safeName,
|
|
1109
1235
|
version: versionField,
|
|
1110
1236
|
target: z.literal("browser"),
|
|
1111
|
-
agent:
|
|
1112
|
-
.object({
|
|
1113
|
-
model: z.string().min(1),
|
|
1114
|
-
instructions: z.string().min(1),
|
|
1115
|
-
})
|
|
1116
|
-
.strict(),
|
|
1237
|
+
agent: pooledSingleAgentSchema,
|
|
1117
1238
|
driver: browserDriverSchema.default({}),
|
|
1118
1239
|
/** Vision-grounding model. Defaults to the agent's primary model. */
|
|
1119
1240
|
groundingModel: z.string().min(1).optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/spec",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "User-facing spec schema (Zod) + YAML parser",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "bun test src"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.2.
|
|
18
|
+
"@crewhaus/errors": "0.2.2",
|
|
19
19
|
"yaml": "^2.6.0",
|
|
20
20
|
"zod": "^3.23.8"
|
|
21
21
|
},
|