@loomwork/reasoning 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,39 @@
1
+ import type { Router } from "./types";
2
+ export declare const tagRouter: Router;
3
+ export interface ArmStats {
4
+ key: string;
5
+ /** Mean reward. Higher = better. */
6
+ rank: number;
7
+ /** How many outcomes produced `rank`. */
8
+ count: number;
9
+ }
10
+ /** Rankings with counts (preferred) or the legacy count-free record. */
11
+ export type Rankings = Record<string, number> | readonly ArmStats[];
12
+ export interface RankedRouterOptions {
13
+ /**
14
+ * Sample floor. Arms with fewer than this many outcomes are treated as UNRANKED
15
+ * rather than as evidence — the fix for "one lucky success at n=1 outranks a
16
+ * proven arm at n=10000 forever". Requires `ArmStats[]`; a count-free record
17
+ * throws at construction rather than silently no-opping the floor.
18
+ */
19
+ minSamples?: number;
20
+ /** Router for unranked/under-sampled requests. Default: cheap-first `tagRouter`. */
21
+ fallback?: Router;
22
+ }
23
+ export declare function rankedRouter(rankings: Rankings, opts?: RankedRouterOptions): Router;
24
+ export interface Ucb1Options {
25
+ /** Exploration weight. 1 = textbook UCB1; lower is greedier, 0 is pure exploit. */
26
+ exploration?: number;
27
+ /** Router for the cold start (no samples at all). Default: cheap-first `tagRouter`. */
28
+ fallback?: Router;
29
+ }
30
+ export declare function ucb1(stats: readonly ArmStats[], opts?: Ucb1Options): Router;
31
+ export interface EpsilonGreedyOptions {
32
+ /** Probability in [0,1] of sampling a uniformly random route instead of exploiting. */
33
+ epsilon: number;
34
+ /** Injected RNG (default `Math.random`). Pass a seeded PRNG for tests and DST runs. */
35
+ rng?: () => number;
36
+ }
37
+ export declare function epsilonGreedy(inner: Router, opts: EpsilonGreedyOptions): Router;
38
+ export declare function fixedRouter(modelId: string): Router;
39
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgC,MAAM,EAAE,MAAM,SAAS,CAAC;AASpE,eAAO,MAAM,SAAS,EAAE,MAYvB,CAAC;AASF,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wEAAwE;AACxE,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,QAAQ,EAAE,CAAC;AAoBpE,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAgBD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,GAAE,mBAAwB,GAAG,MAAM,CAqCvF;AAED,MAAM,WAAW,WAAW;IAC1B,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAYD,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAAE,IAAI,GAAE,WAAgB,GAAG,MAAM,CAoC/E;AAED,MAAM,WAAW,oBAAoB;IACnC,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAUD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAgB/E;AAID,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUnD"}
package/dist/router.js ADDED
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tagRouter = void 0;
4
+ exports.rankedRouter = rankedRouter;
5
+ exports.ucb1 = ucb1;
6
+ exports.epsilonGreedy = epsilonGreedy;
7
+ exports.fixedRouter = fixedRouter;
8
+ // Default routing policy: the lowest-tier route whose tags include the request's
9
+ // strategy, else the lowest-tier route overall. This implements "use the cheap
10
+ // model first; elevate only when confidence is low" (the P3 elevation ladder).
11
+ //
12
+ // Contract: `routes` are expected tier-ascending. ReasoningRuntime owns the sort
13
+ // (once, at construction) and always passes its sorted `routes`, so this router
14
+ // takes the first tag/overall match without re-sorting on every call.
15
+ exports.tagRouter = {
16
+ select(req, routes) {
17
+ if (req.strategy !== undefined) {
18
+ const tagged = routes.find((r) => r.tags?.includes(req.strategy));
19
+ if (tagged !== undefined)
20
+ return tagged;
21
+ }
22
+ const first = routes[0];
23
+ if (first === undefined) {
24
+ throw new Error("ReasoningRuntime: no routes configured");
25
+ }
26
+ return first;
27
+ },
28
+ };
29
+ function indexRankings(rankings) {
30
+ if (Array.isArray(rankings)) {
31
+ const m = new Map();
32
+ for (const s of rankings)
33
+ m.set(s.key, { rank: s.rank, count: s.count });
34
+ return m;
35
+ }
36
+ const m = new Map();
37
+ for (const [key, rank] of Object.entries(rankings))
38
+ m.set(key, { rank });
39
+ return m;
40
+ }
41
+ // M10 self-optimization — a "learned" router. Given a ranking (e.g. from
42
+ // @loomwork/analytics `rankModels`/`outcomeAnalytics`, mined from historical logs),
43
+ // prefer the highest-ranked route that exists, falling back to cheap-first
44
+ // (`tagRouter`) for unranked requests. Used inside ReasoningRuntime with
45
+ // `ctx.decide`, its choice is recorded as a durable decision (P9) — auditable and
46
+ // override-able, never silently autonomous (P10).
47
+ //
48
+ // PURE EXPLOIT. This router never tries an arm to learn about it: an arm absent
49
+ // from `rankings` can never be selected while any other arm is ranked, so a
50
+ // ranking mined from a log that this router itself produced will narrow over time
51
+ // (the arms it stops choosing stop generating outcomes). `minSamples` bounds the
52
+ // damage from thin evidence, but if you want the loop to keep learning, wrap it —
53
+ // `epsilonGreedy(rankedRouter(stats), { epsilon: 0.1 })` — or use `ucb1`, which
54
+ // allocates exploration by uncertainty instead of by coin flip.
55
+ function rankedRouter(rankings, opts = {}) {
56
+ // Index the ranking once at construction; each select is then a single O(R)
57
+ // max-scan instead of an O(R log R) filter+sort per call (routing decisions
58
+ // are on the reasoning hot path). Ties keep the tier-ascending order the
59
+ // caller passes — the first route holding the max rank wins, which is exactly
60
+ // what a stable sort's `[0]` yielded.
61
+ const rank = indexRankings(rankings);
62
+ const minSamples = opts.minSamples ?? 0;
63
+ const fallback = opts.fallback ?? exports.tagRouter;
64
+ if (minSamples > 0) {
65
+ for (const arm of rank.values()) {
66
+ if (arm.count === undefined) {
67
+ throw new Error("rankedRouter: minSamples needs sample counts — pass ArmStats[] " +
68
+ "(e.g. outcomeAnalytics(events)), not a count-free rankings record");
69
+ }
70
+ }
71
+ }
72
+ return {
73
+ select(req, routes) {
74
+ let best;
75
+ let bestRank = Number.NEGATIVE_INFINITY;
76
+ for (const r of routes) {
77
+ const arm = rank.get(r.id);
78
+ if (arm === undefined)
79
+ continue;
80
+ if (arm.count !== undefined && arm.count < minSamples)
81
+ continue;
82
+ if (arm.rank > bestRank) {
83
+ bestRank = arm.rank;
84
+ best = r;
85
+ }
86
+ }
87
+ return best ?? fallback.select(req, routes);
88
+ },
89
+ };
90
+ }
91
+ // UCB1 — the principled fix for the ranked router's collapse, and the recommended
92
+ // default for a live learning loop. Scores each arm `mean + c·sqrt(2·ln N / n)`:
93
+ // the bonus is large while an arm is under-sampled and shrinks as evidence
94
+ // accumulates, so a lucky n=1 arm cannot pin the router (its rivals keep their own
95
+ // large bonus until they are tried too) and an arm with NO samples scores +Inf and
96
+ // is guaranteed a trial. Deterministic — no RNG — which is what you want in a
97
+ // replay-first runtime: the same evidence always yields the same route.
98
+ //
99
+ // Assumes rewards on a comparable scale (the [0,1] of `success`/normalized `score`
100
+ // that `outcomeAnalytics` produces); `exploration` is the knob if yours differ.
101
+ function ucb1(stats, opts = {}) {
102
+ const c = opts.exploration ?? 1;
103
+ const fallback = opts.fallback ?? exports.tagRouter;
104
+ // Fold the evidence once at construction — per-select work is then one O(R)
105
+ // scan with no logarithm (`lnTotal` is loop-invariant across every select).
106
+ const arms = new Map();
107
+ let total = 0;
108
+ for (const s of stats) {
109
+ arms.set(s.key, s);
110
+ total += s.count;
111
+ }
112
+ const lnTotal = Math.log(total);
113
+ return {
114
+ select(req, routes) {
115
+ if (total === 0)
116
+ return fallback.select(req, routes);
117
+ let best;
118
+ let bestScore = Number.NEGATIVE_INFINITY;
119
+ for (const r of routes) {
120
+ const arm = arms.get(r.id);
121
+ // Never sampled → must-try. Ties resolve to the first route, so a cold
122
+ // start walks the tier-ascending ladder one trial at a time.
123
+ const score = arm === undefined || arm.count <= 0
124
+ ? Number.POSITIVE_INFINITY
125
+ : arm.rank + c * Math.sqrt((2 * lnTotal) / arm.count);
126
+ if (score > bestScore) {
127
+ bestScore = score;
128
+ best = r;
129
+ }
130
+ }
131
+ return best ?? fallback.select(req, routes);
132
+ },
133
+ };
134
+ }
135
+ // ε-greedy — the simplest way to stop any exploit-only router from starving its
136
+ // own evidence: with probability ε ignore `inner` and sample a route uniformly.
137
+ //
138
+ // The randomness is replay-safe WHERE IT MATTERS: ReasoningRuntime resolves the
139
+ // route inside `ctx.decide`, so the sampled id is committed as DECISION_RECORDED
140
+ // and replay serves that recording back — `select` is never called again and the
141
+ // RNG is never re-rolled. Standalone callers (no `decide` sink) get no such
142
+ // guarantee; inject `rng` if you need them reproducible.
143
+ function epsilonGreedy(inner, opts) {
144
+ const { epsilon } = opts;
145
+ if (!(epsilon >= 0 && epsilon <= 1)) {
146
+ throw new Error(`epsilonGreedy: epsilon must be in [0,1], got ${epsilon}`);
147
+ }
148
+ const rng = opts.rng ?? Math.random;
149
+ return {
150
+ select(req, routes) {
151
+ if (routes.length > 0 && rng() < epsilon) {
152
+ const picked = routes[Math.min(routes.length - 1, Math.floor(rng() * routes.length))];
153
+ if (picked !== undefined)
154
+ return picked;
155
+ }
156
+ return inner.select(req, routes);
157
+ },
158
+ };
159
+ }
160
+ // A fixed router that always selects a specific model id — handy for tests and for
161
+ // pinning a strategy to one model.
162
+ function fixedRouter(modelId) {
163
+ return {
164
+ select(_req, routes) {
165
+ const route = routes.find((r) => r.id === modelId);
166
+ if (route === undefined) {
167
+ throw new Error(`ReasoningRuntime: fixedRouter target "${modelId}" not in routes`);
168
+ }
169
+ return route;
170
+ },
171
+ };
172
+ }
173
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=routing.bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.bench.d.ts","sourceRoot":"","sources":["../src/routing.bench.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const ReasoningRuntime_1 = require("./ReasoningRuntime");
5
+ const router_1 = require("./router");
6
+ // Benches for the two routing hot paths made O(1)/O(n):
7
+ // 1. tagRouter no longer re-sorts the route list on every select().
8
+ // 2. Elevation hops via a precomputed next-tier index instead of an O(n) scan,
9
+ // so walking a deep ladder is O(n) not O(n^2).
10
+ class FakeProvider {
11
+ res;
12
+ constructor(res) {
13
+ this.res = res;
14
+ }
15
+ async complete() {
16
+ return this.res;
17
+ }
18
+ }
19
+ const weak = { content: "weak", stopReason: "max_tokens" };
20
+ // Force elevation all the way up the ladder every call.
21
+ const alwaysLow = { score: () => 0 };
22
+ function makeRoutes(n) {
23
+ const provider = new FakeProvider(weak);
24
+ return Array.from({ length: n }, (_, i) => ({
25
+ id: `m${i}`,
26
+ provider,
27
+ tier: i,
28
+ ...(i === 0 && { tags: ["planning"] }),
29
+ }));
30
+ }
31
+ const routes = makeRoutes(64);
32
+ (0, vitest_1.describe)("tagRouter.select over 64 routes", () => {
33
+ const req = { messages: [{ role: "user", content: "hi" }], strategy: "planning" };
34
+ (0, vitest_1.bench)("re-sort baseline (previous shape)", () => {
35
+ const sorted = [...routes].sort((a, b) => a.tier - b.tier);
36
+ sorted.find((r) => r.tags?.includes("planning"));
37
+ });
38
+ (0, vitest_1.bench)("tagRouter.select (pre-sorted, no re-sort)", () => {
39
+ router_1.tagRouter.select(req, routes);
40
+ });
41
+ });
42
+ (0, vitest_1.describe)("reason() full elevation up a 64-tier ladder", () => {
43
+ const runtime = new ReasoningRuntime_1.ReasoningRuntime({ routes, confidence: alwaysLow });
44
+ (0, vitest_1.bench)("reason (O(1) tier hops)", async () => {
45
+ await runtime.reason({ messages: [{ role: "user", content: "hi" }] });
46
+ });
47
+ });
48
+ //# sourceMappingURL=routing.bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.bench.js","sourceRoot":"","sources":["../src/routing.bench.ts"],"names":[],"mappings":";;AACA,mCAAyC;AACzC,yDAAsD;AACtD,qCAAqC;AAGrC,wDAAwD;AACxD,sEAAsE;AACtE,iFAAiF;AACjF,oDAAoD;AAEpD,MAAM,YAAY;IACa;IAA7B,YAA6B,GAAgB;QAAhB,QAAG,GAAH,GAAG,CAAa;IAAG,CAAC;IACjD,KAAK,CAAC,QAAQ;QACZ,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;CACF;AAED,MAAM,IAAI,GAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC;AACxE,wDAAwD;AACxD,MAAM,SAAS,GAAqB,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;AAEvD,SAAS,UAAU,CAAC,CAAS;IAC3B,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1C,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ;QACR,IAAI,EAAE,CAAC;QACP,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;KACvC,CAAC,CAAC,CAAC;AACN,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC;AAE9B,IAAA,iBAAQ,EAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IAE3F,IAAA,cAAK,EAAC,mCAAmC,EAAE,GAAG,EAAE;QAC9C,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,IAAA,cAAK,EAAC,2CAA2C,EAAE,GAAG,EAAE;QACtD,kBAAS,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,6CAA6C,EAAE,GAAG,EAAE;IAC3D,MAAM,OAAO,GAAG,IAAI,mCAAgB,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC;IAExE,IAAA,cAAK,EAAC,yBAAyB,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,134 @@
1
+ import type { ContentResolveContext, ContentResolver, LlmContent, LlmMessageInput, LlmProvider, LlmResponse, LlmToolCall, LlmToolSpecInput } from "@loomwork/llm";
2
+ export type DecideFn = <T = unknown>(decisionId: string, decider: () => T | Promise<T>) => Promise<T>;
3
+ export interface ModelRoute {
4
+ id: string;
5
+ provider: LlmProvider;
6
+ tier: number;
7
+ /** Strategy hints (e.g. "planning", "coding", "review") for tag-based routing. */
8
+ tags?: string[];
9
+ }
10
+ export interface ReasoningRequest {
11
+ messages: LlmMessageInput[];
12
+ system?: LlmContent;
13
+ /** Picks a route by tag and namespaces the logged decisions. */
14
+ strategy?: string;
15
+ tools?: LlmToolSpecInput[];
16
+ maxTokens?: number;
17
+ temperature?: number;
18
+ signal?: AbortSignal;
19
+ /** Budget cap — never elevate above this tier. Default: the highest tier available. */
20
+ maxTier?: number;
21
+ }
22
+ export interface ReasoningTraceEntry {
23
+ modelId: string;
24
+ tier: number;
25
+ confidence: number;
26
+ elevated: boolean;
27
+ }
28
+ export interface ReasoningResult {
29
+ content: string;
30
+ toolCalls?: LlmToolCall[];
31
+ stopReason: LlmResponse["stopReason"];
32
+ usage?: LlmResponse["usage"];
33
+ /**
34
+ * ADR 0133 - the answering route's reasoning, as the call returned it. Through an effect-backed
35
+ * `invoke` these are recorded `LlmReasoningPart`s; a caller keeps only those (`type: "reasoning"`).
36
+ */
37
+ reasoning?: LlmResponse["reasoning"];
38
+ /** ADR 0133 D5 - the answering call's block order (positions only), so a transcript can reproduce the vendor sequence. */
39
+ layout?: LlmResponse["layout"];
40
+ modelId: string;
41
+ confidence: number;
42
+ elevated: boolean;
43
+ trace: ReasoningTraceEntry[];
44
+ }
45
+ export interface ConfidenceSignal {
46
+ score(req: ReasoningRequest, res: LlmResponse): number | Promise<number>;
47
+ }
48
+ export interface Router {
49
+ select(req: ReasoningRequest, routes: ModelRoute[]): ModelRoute;
50
+ }
51
+ export interface ReasoningCache {
52
+ get(key: string): LlmResponse | undefined;
53
+ set(key: string, value: LlmResponse): void;
54
+ }
55
+ /**
56
+ * The request a {@link ModelInvoker} receives: the reasoning request minus the routing-only
57
+ * fields and minus `signal` (a signal is not data — an effect-backed invoker gets its
58
+ * cancellation from the effect context, not from here).
59
+ */
60
+ export interface ModelCallRequest {
61
+ messages: LlmMessageInput[];
62
+ system?: LlmContent;
63
+ tools?: LlmToolSpecInput[];
64
+ maxTokens?: number;
65
+ temperature?: number;
66
+ }
67
+ /**
68
+ * R9 — how a chosen route's model call is MADE. Without one, the runtime calls
69
+ * `route.provider.complete` directly, which is outside the effect ring (no K2 authorization,
70
+ * no metering, no kill switch). Supplying an invoker hands the call to the host so it can run
71
+ * as an effect: `agentLoop$` passes one that resolves each call through `ctx.execute`, so the
72
+ * call is authorized, metered, kill-switched and `guardRequest`-checked, and is JOURNALED —
73
+ * a replay serves the recorded response instead of re-calling the model.
74
+ *
75
+ * The runtime's response cache is BYPASSED when an invoker is supplied: a cache hit would skip
76
+ * the invoker, so whether a durable frame exists would depend on a process-local cache and a
77
+ * replay could diverge from the run.
78
+ */
79
+ export type ModelInvoker = (route: ModelRoute, request: ModelCallRequest) => Promise<LlmResponse>;
80
+ export interface ReasonOptionsBase {
81
+ /** When provided, route/elevation choices are recorded through it (P9). */
82
+ decide?: DecideFn;
83
+ /** R9 — make each model call through the host instead of the route's provider. See {@link ModelInvoker}. */
84
+ invoke?: ModelInvoker;
85
+ /**
86
+ * R10 — partition the response cache. The cache key is the request as written plus the route; two
87
+ * callers with an identical prompt would otherwise share a cached response. The runtime already
88
+ * adds the `resolveContext.principal` identity (tenant, actor, onBehalfOf) when it has one; a host
89
+ * that cannot hand over a principal (a runner body cannot read its own) passes a stable scope
90
+ * string here, e.g. the tenant id. Unset on a single-tenant host = one shared cache, as before.
91
+ *
92
+ * NOTE on metering: a cache hit makes no provider call, so on the direct path (no effect ring) it
93
+ * is not metered. That is the cache's purpose; a host that must meter every request routes the
94
+ * call through an effect (`invoke`), which bypasses this cache.
95
+ */
96
+ cacheScope?: string;
97
+ /**
98
+ * R10 — journal the live provider call. Unlike {@link invoke} (which REPLACES the call so the host
99
+ * can run it as an effect), this wraps it: the runtime still guards the request, checks the route's
100
+ * modalities and keys its response cache on the request as written, and hands the host only the
101
+ * part that touches the network (cache lookup, content resolution, `provider.complete`, cache
102
+ * fill) as `run`. The host records `run`'s result (`ctx.decide`) so a replay serves it and `run` is
103
+ * never called. Ignored when `invoke` is set.
104
+ */
105
+ wrapCall?: (route: ModelRoute, run: () => Promise<LlmResponse>) => Promise<LlmResponse>;
106
+ signal?: AbortSignal;
107
+ /** Namespaces decision ids so multiple reason() calls in one execution don't collide. */
108
+ decisionKey?: string;
109
+ }
110
+ /**
111
+ * R5 — fetch the bytes behind a media `ref` for the DIRECT (non-`invoke`) path, after the chosen
112
+ * route's capabilities have been checked and after the cache lookup (the cache is keyed on the
113
+ * request as written, i.e. on addresses, never on resolved bytes). Without it a ref is refused.
114
+ *
115
+ * `resolveContent` and `resolveContext` come TOGETHER (the type makes one without the other an
116
+ * error): the context is what lets a store authorize the read, and a resolver MUST deny a read whose
117
+ * `executionId` / `principal` it cannot attribute.
118
+ */
119
+ export type ReasonOptions = ReasonOptionsBase & ({
120
+ resolveContent?: undefined;
121
+ resolveContext?: undefined;
122
+ } | {
123
+ resolveContent: ContentResolver;
124
+ resolveContext: Pick<ContentResolveContext, "executionId" | "workflowId" | "stepId" | "principal">;
125
+ });
126
+ export interface ReasoningRuntimeConfig {
127
+ routes: ModelRoute[];
128
+ router?: Router;
129
+ confidence?: ConfidenceSignal;
130
+ /** Elevate while confidence < threshold and a higher tier is available. Default 0.5. */
131
+ elevateThreshold?: number;
132
+ cache?: ReasoningCache;
133
+ }
134
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@loomwork/reasoning",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Reasoning runtime for Loom — model routing, confidence-based elevation, and critique as composable stages, with route decisions logged (P9)",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@loomwork/llm": "^0.1.1",
16
+ "@loomwork/classify": "^0.1.1"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "typescript": "^5.6.3",
21
+ "vitest": "^2.1.8"
22
+ },
23
+ "license": "Apache-2.0",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/chhetrisushil/loom.git",
27
+ "directory": "packages/reasoning"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "!src/**/*.test.ts",
33
+ "!src/**/*.test.tsx",
34
+ "!src/**/__tests__",
35
+ "!src/**/__fixtures__",
36
+ "!src/**/__snapshots__",
37
+ "!src/**/__golden__",
38
+ "LICENSE",
39
+ "NOTICE"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "tag": "next"
44
+ },
45
+ "engines": {
46
+ "node": ">=20.0.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc --build",
50
+ "dev": "tsc --build --watch",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "bench": "vitest bench --run",
54
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
55
+ "typecheck:tests": "tsc -p tsconfig.test.json"
56
+ }
6
57
  }