@gobing-ai/ts-ai-decision 0.5.30 → 0.5.33

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/hub.js CHANGED
@@ -17,6 +17,38 @@ import { DecisionMakerRegistry } from './registry.js';
17
17
  export const DEFAULT_MIN_CONFIDENCE = 0.7;
18
18
  /** Hub default when neither the per-call options, the decision nor the catalog names a maker. */
19
19
  export const DEFAULT_MAKER = 'typesafe';
20
+ /** Upper bound on a carried error message, in characters (ADR-040). */
21
+ export const DECISION_ERROR_MESSAGE_MAX = 500;
22
+ /**
23
+ * Reduce a thrown value to {@link DecisionErrorInfo}. Subclasses of `DecisionError` set their own
24
+ * `name`; a subclass that does not is named by its constructor rather than by `'Error'`.
25
+ */
26
+ function toErrorInfo(error) {
27
+ if (!(error instanceof Error)) {
28
+ return { name: 'NonError', message: String(error).slice(0, DECISION_ERROR_MESSAGE_MAX) };
29
+ }
30
+ const named = error.name !== 'Error' ? error.name : (error.constructor?.name ?? error.name);
31
+ return { name: named, message: error.message.slice(0, DECISION_ERROR_MESSAGE_MAX) };
32
+ }
33
+ /**
34
+ * Normalize a {@link MakerRef} to an ordered chain. Empty and duplicate lists are caller errors
35
+ * here, not at load: the loader's schema already rejects them in a catalog.
36
+ */
37
+ function toChain(ref) {
38
+ if (typeof ref === 'string')
39
+ return [ref];
40
+ if (ref.length === 0) {
41
+ throw new UnknownDecisionMakerError('A maker chain must name at least one maker', '');
42
+ }
43
+ const seen = new Set();
44
+ for (const name of ref) {
45
+ if (seen.has(name)) {
46
+ throw new UnknownDecisionMakerError(`Maker chain repeats '${name}'`, name);
47
+ }
48
+ seen.add(name);
49
+ }
50
+ return [...ref];
51
+ }
20
52
  /** Effective confidence floor: decision → catalog defaults → package default. */
21
53
  function effectiveMinConfidence(definition, defaults) {
22
54
  return definition.minConfidence ?? defaults.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
@@ -75,15 +107,19 @@ export class DecisionHub {
75
107
  }
76
108
  /**
77
109
  * Register every decision of an already-loaded catalog. All-or-nothing: a
78
- * duplicate decision id or an unregistered maker name (decision or
79
- * `defaults.maker`) throws {@link DecisionCatalogError} and registers
110
+ * duplicate decision id or an unregistered maker name (any chain name of a
111
+ * decision or `defaults.maker`, or a decision `shadow`) throws
112
+ * {@link DecisionCatalogError} and registers
80
113
  * nothing from the offending catalog.
81
114
  */
82
115
  load(catalog) {
83
116
  const defaultsMaker = catalog.defaults.maker;
84
- if (defaultsMaker !== undefined && !this.registry.has(defaultsMaker)) {
85
- throw new DecisionCatalogError(`Catalog "${catalog.source}" defaults.maker '${defaultsMaker}' is not registered in the hub registry ` +
86
- `(registered: ${this.registry.names().join(', ') || 'none'})`, catalog.source, undefined, 'maker');
117
+ const defaultsChain = defaultsMaker === undefined ? undefined : toChain(defaultsMaker);
118
+ for (const name of defaultsChain ?? []) {
119
+ if (!this.registry.has(name)) {
120
+ throw new DecisionCatalogError(`Catalog "${catalog.source}" defaults.maker '${name}' is not registered in the hub registry ` +
121
+ `(registered: ${this.registry.names().join(', ') || 'none'})`, catalog.source, undefined, 'maker');
122
+ }
87
123
  }
88
124
  // Stage first, commit only when every decision passed the cross-catalog checks.
89
125
  const staged = [];
@@ -93,9 +129,15 @@ export class DecisionHub {
93
129
  throw new DecisionCatalogError(`Decision id "${id}" in catalog "${catalog.source}" duplicates the decision loaded from ` +
94
130
  `"${existing.definition.source}"`, catalog.source, id, 'id');
95
131
  }
96
- if (definition.maker !== undefined && !this.registry.has(definition.maker)) {
97
- throw new DecisionCatalogError(`Decision "${id}" in catalog "${catalog.source}" names maker '${definition.maker}' that is not ` +
98
- `registered in the hub registry (registered: ${this.registry.names().join(', ') || 'none'})`, catalog.source, id, 'maker');
132
+ for (const name of definition.maker === undefined ? [] : toChain(definition.maker)) {
133
+ if (!this.registry.has(name)) {
134
+ throw new DecisionCatalogError(`Decision "${id}" in catalog "${catalog.source}" names maker '${name}' that is not ` +
135
+ `registered in the hub registry (registered: ${this.registry.names().join(', ') || 'none'})`, catalog.source, id, 'maker');
136
+ }
137
+ }
138
+ if (definition.shadow !== undefined && !this.registry.has(definition.shadow)) {
139
+ throw new DecisionCatalogError(`Decision "${id}" in catalog "${catalog.source}" names shadow '${definition.shadow}' that is not ` +
140
+ `registered in the hub registry (registered: ${this.registry.names().join(', ') || 'none'})`, catalog.source, id, 'shadow');
99
141
  }
100
142
  staged.push({ definition, defaults: catalog.defaults });
101
143
  }
@@ -118,6 +160,7 @@ export class DecisionHub {
118
160
  /** Pure read of one decision's served contract; throws UnknownDecisionError for an unknown id. */
119
161
  describe(id) {
120
162
  const { definition, defaults } = this.mustGet(id);
163
+ const chain = this.effectiveChain(definition, defaults);
121
164
  const model = definition.model ?? defaults.model;
122
165
  return {
123
166
  id: definition.id,
@@ -128,15 +171,18 @@ export class DecisionHub {
128
171
  criteria: definition.criteria,
129
172
  fallback: definition.fallback,
130
173
  minConfidence: effectiveMinConfidence(definition, defaults),
131
- maker: this.effectiveMakerName(definition, defaults),
174
+ maker: chain[0] ?? DEFAULT_MAKER,
175
+ makers: chain,
176
+ ...(definition.shadow !== undefined ? { shadow: definition.shadow } : {}),
132
177
  ...(model !== undefined ? { model } : {}),
133
178
  };
134
179
  }
135
180
  /**
136
181
  * Serve one decision with a fallback-guaranteed answer. Resolves the input,
137
182
  * renders exactly one Jev question keyed by the decision id, selects the
138
- * maker by name (per call → decision → catalog defaults → hub default) and
139
- * issues a single `maker.ask`. Backend construction, transport, timeout,
183
+ * maker chain (per call → decision → catalog defaults → hub default) and
184
+ * asks its makers in order until one answers, with an optional shadow asked
185
+ * beside it (ADR-040). Backend construction, transport, timeout,
140
186
  * invalid answers and low confidence all resolve to the declared fallback;
141
187
  * only caller mistakes throw, before any backend call.
142
188
  */
@@ -144,12 +190,20 @@ export class DecisionHub {
144
190
  const start = this.now();
145
191
  const { definition, defaults } = this.mustGet(id); // UnknownDecisionError — caller error
146
192
  const params = resolveDecisionInput(definition, input); // DecisionInputError — caller error
147
- const makerName = options.maker ?? definition.maker ?? defaults.maker ?? this.defaultMaker;
148
- if (!this.registry.has(makerName)) {
149
- // Reachable only for a per-call name: catalog and hub default names are checked at
150
- // load/construct. It is a caller error, thrown before any backend interaction.
151
- throw new UnknownDecisionMakerError(`Decision "${id}" was asked on maker '${makerName}' that is not registered in the hub registry ` +
152
- `(registered: ${this.registry.names().join(', ') || 'none'})`, makerName);
193
+ const chain = options.maker === undefined ? this.effectiveChain(definition, defaults) : toChain(options.maker);
194
+ for (const name of chain) {
195
+ // Reachable only for a per-call chain: catalog and hub default names are checked at
196
+ // load/construct. A caller error, thrown before any backend interaction.
197
+ if (!this.registry.has(name)) {
198
+ throw new UnknownDecisionMakerError(`Decision "${id}" was asked on maker '${name}' that is not registered in the hub registry ` +
199
+ `(registered: ${this.registry.names().join(', ') || 'none'})`, name);
200
+ }
201
+ }
202
+ const shadowName = options.shadow ?? definition.shadow;
203
+ if (shadowName !== undefined && !this.registry.has(shadowName)) {
204
+ // Same caller-error rule as a per-call maker name: before any backend interaction.
205
+ throw new UnknownDecisionMakerError(`Decision "${id}" was asked with shadow '${shadowName}' that is not registered in the hub registry ` +
206
+ `(registered: ${this.registry.names().join(', ') || 'none'})`, shadowName);
153
207
  }
154
208
  const state = buildState(definition, params);
155
209
  const model = definition.model ?? defaults.model;
@@ -158,6 +212,75 @@ export class DecisionHub {
158
212
  questions: { [definition.id]: buildJevQuestion(renderQuestion(definition, params)) },
159
213
  ...(model !== undefined ? { model } : {}),
160
214
  };
215
+ // Shadow rule (ADR-040): start the shadow on the SAME request object, without awaiting it, so
216
+ // both paths run concurrently and latency is the slower of the two. It never rejects `decide`
217
+ // and never touches a served field — a rejected or failed ask maps to its own outcome.
218
+ const shadowStart = this.now();
219
+ const shadowTask = shadowName === undefined
220
+ ? undefined
221
+ : this.askOne(shadowName, definition, defaults, request, shadowStart).then((settled) => settled, (error) => ({
222
+ result: this.fallbackResult(definition, shadowName, 'error', null, shadowStart, undefined, toErrorInfo(error)),
223
+ failed: true,
224
+ }));
225
+ // Chain rule (ADR-040): ask makers in order and stop at the first that did not fail as a
226
+ // backend — `accepted` and `low-confidence` both answer. Every maker after the answering one
227
+ // stays unresolved, so its lazy factory never runs.
228
+ const attempts = [];
229
+ let last;
230
+ for (const makerName of chain) {
231
+ const attemptStart = this.now();
232
+ last = await this.askOne(makerName, definition, defaults, request, attemptStart);
233
+ attempts.push({
234
+ maker: makerName,
235
+ reason: last.result.reason,
236
+ durationMs: this.now() - attemptStart,
237
+ ...(last.result.error !== undefined ? { error: last.result.error } : {}),
238
+ });
239
+ if (!last.failed)
240
+ break;
241
+ }
242
+ // `toChain` rejects an empty list, so the loop always ran at least once.
243
+ const final = last?.result;
244
+ if (final === undefined) {
245
+ throw new UnknownDecisionMakerError(`Decision "${id}" has no maker to ask`, id);
246
+ }
247
+ // `durationMs` covers the whole call, shadow wait included; a single-maker call without a
248
+ // shadow keeps its exact previous shape.
249
+ const shadowOutcome = shadowTask === undefined ? undefined : this.toShadowOutcome(await shadowTask);
250
+ return {
251
+ ...final,
252
+ durationMs: this.now() - start,
253
+ ...(attempts.length > 1 ? { attempts } : {}),
254
+ ...(shadowOutcome !== undefined ? { shadow: shadowOutcome } : {}),
255
+ };
256
+ }
257
+ /**
258
+ * Project a shadow's own result onto {@link ShadowOutcome}. `value` is carried only when the shadow
259
+ * actually answered — a served-model answer or a low-confidence one — never its declared fallback.
260
+ */
261
+ toShadowOutcome(outcome) {
262
+ const { result } = outcome;
263
+ const answered = result.source === 'model' || result.reason === 'low-confidence';
264
+ return {
265
+ maker: result.maker,
266
+ ...(answered && outcome.answered !== undefined ? { value: outcome.answered } : {}),
267
+ confidence: result.confidence,
268
+ reason: result.reason,
269
+ durationMs: result.durationMs,
270
+ ...(result.error !== undefined ? { error: result.error } : {}),
271
+ };
272
+ }
273
+ /**
274
+ * Ask one maker and classify its outcome. `failed` is true exactly for `no-backend`, `timeout`
275
+ * and `error` — the reasons a chain advances past (ADR-040); `accepted` and `low-confidence`
276
+ * answer, so the chain stops there.
277
+ */
278
+ async askOne(makerName, definition, defaults, request, start) {
279
+ const outcome = (result, answered) => ({
280
+ result,
281
+ failed: result.reason === 'no-backend' || result.reason === 'timeout' || result.reason === 'error',
282
+ ...(answered !== undefined ? { answered } : {}),
283
+ });
161
284
  // The one auditable try/catch for steps 4–5: a resolve failure is a construction
162
285
  // failure (no-backend); ask failures map per the error taxonomy. Neither rejects.
163
286
  let inAsk = false;
@@ -171,11 +294,21 @@ export class DecisionHub {
171
294
  validateAnswers(request.questions, answers);
172
295
  const answer = answers[definition.id];
173
296
  if (answer === undefined || answer.kind !== definition.type) {
174
- return this.fallbackResult(definition, makerName, 'error', null, start);
297
+ // Nothing was thrown here, so the failure is synthesized: every `error` reason
298
+ // must carry a cause a consumer can log (ADR-040).
299
+ return outcome(this.fallbackResult(definition, makerName, 'error', null, start, undefined, {
300
+ name: 'DecisionBackendError',
301
+ message: `Maker returned no answer of type ${definition.type} for "${definition.id}"`,
302
+ }));
175
303
  }
176
304
  const observed = observedAnswer(answer);
305
+ const answered = answer.kind === 'choice'
306
+ ? answer.label
307
+ : answer.kind === 'score'
308
+ ? answer.score
309
+ : answer.probability >= 0.5;
177
310
  if (observed.confidence < effectiveMinConfidence(definition, defaults)) {
178
- return this.fallbackResult(definition, makerName, 'low-confidence', observed.confidence, start, observed.probability);
311
+ return outcome(this.fallbackResult(definition, makerName, 'low-confidence', observed.confidence, start, observed.probability), answered);
179
312
  }
180
313
  const base = {
181
314
  id: definition.id,
@@ -187,16 +320,16 @@ export class DecisionHub {
187
320
  };
188
321
  switch (answer.kind) {
189
322
  case 'choice':
190
- return { ...base, type: 'choice', value: answer.label };
323
+ return outcome({ ...base, type: 'choice', value: answer.label }, answered);
191
324
  case 'score':
192
- return { ...base, type: 'score', value: answer.score };
325
+ return outcome({ ...base, type: 'score', value: answer.score }, answered);
193
326
  case 'noul':
194
- return {
327
+ return outcome({
195
328
  ...base,
196
329
  type: 'noul',
197
330
  value: answer.probability >= 0.5,
198
331
  probability: answer.probability,
199
- };
332
+ }, answered);
200
333
  }
201
334
  }
202
335
  catch (error) {
@@ -205,12 +338,13 @@ export class DecisionHub {
205
338
  : error instanceof DecisionTimeoutError
206
339
  ? 'timeout'
207
340
  : 'error';
208
- return this.fallbackResult(definition, makerName, reason, null, start);
341
+ return outcome(this.fallbackResult(definition, makerName, reason, null, start, undefined, toErrorInfo(error)));
209
342
  }
210
343
  }
211
- /** Maker-name precedence for discovery results: decision → catalog defaults → hub default. */
212
- effectiveMakerName(definition, defaults) {
213
- return definition.maker ?? defaults.maker ?? this.defaultMaker;
344
+ /** Maker-chain precedence for discovery results: decision → catalog defaults → hub default. */
345
+ effectiveChain(definition, defaults) {
346
+ const ref = definition.maker ?? defaults.maker;
347
+ return ref === undefined ? [this.defaultMaker] : toChain(ref);
214
348
  }
215
349
  mustGet(id) {
216
350
  const registered = this.decisions.get(id);
@@ -225,7 +359,7 @@ export class DecisionHub {
225
359
  * confidence (null when none), always the selected maker name. For noul,
226
360
  * `probability` stays null unless the answer itself supplied one.
227
361
  */
228
- fallbackResult(definition, makerName, reason, confidence, start, probability) {
362
+ fallbackResult(definition, makerName, reason, confidence, start, probability, error) {
229
363
  const base = {
230
364
  id: definition.id,
231
365
  confidence,
@@ -233,6 +367,7 @@ export class DecisionHub {
233
367
  reason,
234
368
  maker: makerName,
235
369
  durationMs: this.now() - start,
370
+ ...(error !== undefined ? { error } : {}),
236
371
  };
237
372
  switch (definition.type) {
238
373
  case 'choice':
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './agent-driver';
1
2
  export * from './catalog';
2
3
  export * from './errors';
3
4
  export * from './hub';
@@ -5,4 +6,5 @@ export * from './params';
5
6
  export * from './registry';
6
7
  export * from './schema';
7
8
  export * from './types';
9
+ export * from './typesafe-driver';
8
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './agent-driver.js';
1
2
  export * from './catalog.js';
2
3
  export * from './errors.js';
3
4
  export * from './hub.js';
@@ -5,3 +6,4 @@ export * from './params.js';
5
6
  export * from './registry.js';
6
7
  export * from './schema.js';
7
8
  export * from './types.js';
9
+ export * from './typesafe-driver.js';
@@ -1,16 +1,53 @@
1
- import { type DecisionMaker, type DecisionMakerOptions } from '@gobing-ai/ts-ai-runner';
1
+ import { type DecisionDriver, type DecisionMaker, type DecisionMakerOptions } from '@gobing-ai/ts-ai-runner';
2
2
  import { type FmDriverOptions } from '@gobing-ai/ts-decision-fm';
3
- import { type LayaDriverOptions } from '@gobing-ai/ts-laya-mlx';
3
+ import { type AgentDriverOptions } from './agent-driver';
4
+ /**
5
+ * Options forwarded to `createLayaDriver`. Same fields as `LayaDriverOptions` in
6
+ * `@gobing-ai/ts-laya-mlx`. Declared here so published `.d.ts` files do not import
7
+ * that package: it is an optional, macOS-only dependency (ADR-041) and is absent
8
+ * on other platforms.
9
+ */
10
+ export interface LayaLocalDriverOptions {
11
+ pythonPath?: string;
12
+ module?: string;
13
+ workerScript?: string;
14
+ cwd?: string;
15
+ modelId?: string;
16
+ modelPath?: string;
17
+ revision?: string;
18
+ cacheDir?: string;
19
+ dtype?: 'float32' | 'float16' | 'bfloat16';
20
+ batchSize?: number;
21
+ startupTimeoutMs?: number;
22
+ requestTimeoutMs?: number;
23
+ env?: Record<string, string | undefined>;
24
+ platform?: string;
25
+ arch?: string;
26
+ core?: 'mlx-python' | 'mlx-native' | 'auto';
27
+ /** Same role as `LayaDriverOptions.client`. Typed wide so the optional module's class stays assignable. */
28
+ client?: unknown;
29
+ /** Same role as `LayaDriverOptions.nativeLoader`. */
30
+ nativeLoader?: unknown;
31
+ }
32
+ /**
33
+ * Load the bundled Laya driver. The package is an `optionalDependency` with
34
+ * `"os": ["darwin"]`, so non-macOS installs do not contain it. Only the import
35
+ * failure is rewritten; factory errors propagate unchanged.
36
+ *
37
+ * @internal Tests pass `load` because a workspace link always resolves.
38
+ */
39
+ export declare function resolveBundledLayaDriver(options?: LayaLocalDriverOptions, load?: () => Promise<unknown>): Promise<DecisionDriver>;
4
40
  /** A maker, or a lazy factory for one (sync or async). Factories run on first `resolve` and are memoised on success. */
5
41
  export type MakerSource = DecisionMaker | (() => DecisionMaker | Promise<DecisionMaker>);
6
- /** Options for the three built-in makers pre-registered by {@link DecisionMakerRegistry} (task 0091 R2). */
42
+ /** Options for the four built-in makers pre-registered by {@link DecisionMakerRegistry} (task 0091 R2, task 0115 R8). */
7
43
  export interface BuiltinMakerOptions {
8
44
  /** Forwarded to `createDecisionMaker` for every built-in (env, apiKey, baseURL, timeoutMs, ...). */
9
45
  readonly makerOptions?: Omit<DecisionMakerOptions, 'driver' | 'backend' | 'model'>;
10
- /** Driver options for the bundled local drivers; the fm platform check runs at first resolve, not registration. */
46
+ /** Driver options for the bundled drivers; the fm platform check runs at first resolve, not registration. */
11
47
  readonly driverOptions?: {
12
48
  readonly 'fm-local'?: FmDriverOptions;
13
- readonly 'laya-local'?: LayaDriverOptions;
49
+ readonly 'laya-local'?: LayaLocalDriverOptions;
50
+ readonly 'llm-agent'?: AgentDriverOptions;
14
51
  };
15
52
  }
16
53
  /**
@@ -24,7 +61,7 @@ export interface BuiltinMakerOptions {
24
61
  */
25
62
  export declare class DecisionMakerRegistry {
26
63
  private readonly entries;
27
- /** Pre-registers `typesafe`, `fm-local` and `laya-local` unless `builtins: false`. */
64
+ /** Pre-registers `typesafe`, `fm-local`, `laya-local` and `llm-agent` unless `builtins: false`. */
28
65
  constructor(options?: {
29
66
  builtins?: BuiltinMakerOptions | false;
30
67
  });
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC7G,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAGlF,wHAAwH;AACxH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;AAEzF,4GAA4G;AAC5G,MAAM,WAAW,mBAAmB;IAChC,oGAAoG;IACpG,QAAQ,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;IACnF,mHAAmH;IACnH,QAAQ,CAAC,aAAa,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;QAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;CACjH;AAID;;;;;;;;GAQG;AACH,qBAAa,qBAAqB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGpB;IAEJ,sFAAsF;gBAC1E,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAA;KAAE;IAqBhE,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAWjD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,KAAK,IAAI,MAAM,EAAE;IAIjB;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA4BnD;;;;;OAKG;IACG,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAc9C"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE5B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,gBAAgB,CAAC;AAI5E;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,MAAM,CAAC;IAC5C,2GAA2G;IAC3G,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,YAAY,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC1C,OAAO,GAAE,sBAA2B,EACpC,IAAI,GAAE,MAAM,OAAO,CAAC,OAAO,CAA0C,GACtE,OAAO,CAAC,cAAc,CAAC,CAmBzB;AAED,wHAAwH;AACxH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;AAEzF,yHAAyH;AACzH,MAAM,WAAW,mBAAmB;IAChC,oGAAoG;IACpG,QAAQ,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;IACnF,6GAA6G;IAC7G,QAAQ,CAAC,aAAa,CAAC,EAAE;QACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;QACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,sBAAsB,CAAC;QAC/C,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC;KAC7C,CAAC;CACL;AAID;;;;;;;;GAQG;AACH,qBAAa,qBAAqB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGpB;IAEJ,mGAAmG;gBACvF,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAA;KAAE;IAgDhE,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAWjD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,KAAK,IAAI,MAAM,EAAE;IAIjB;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA4BnD;;;;;OAKG;IACG,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAc9C"}
package/dist/registry.js CHANGED
@@ -1,7 +1,30 @@
1
- import { createDecisionMaker } from '@gobing-ai/ts-ai-runner';
1
+ import { createDecisionMaker, DecisionConfigError, resolveApiKey, } from '@gobing-ai/ts-ai-runner';
2
2
  import { createFmDriver } from '@gobing-ai/ts-decision-fm';
3
- import { createLayaDriver } from '@gobing-ai/ts-laya-mlx';
3
+ import { createAgentDriver } from './agent-driver.js';
4
4
  import { DecisionRegistryError, UnknownDecisionMakerError } from './errors.js';
5
+ import { createTypesafeDriver } from './typesafe-driver.js';
6
+ const LAYA_DRIVER_PACKAGE = '@gobing-ai/ts-laya-mlx';
7
+ /**
8
+ * Load the bundled Laya driver. The package is an `optionalDependency` with
9
+ * `"os": ["darwin"]`, so non-macOS installs do not contain it. Only the import
10
+ * failure is rewritten; factory errors propagate unchanged.
11
+ *
12
+ * @internal Tests pass `load` because a workspace link always resolves.
13
+ */
14
+ export async function resolveBundledLayaDriver(options = {}, load = () => import('@gobing-ai/ts-laya-mlx')) {
15
+ let loaded;
16
+ try {
17
+ loaded = await load();
18
+ }
19
+ catch (cause) {
20
+ throw new DecisionConfigError(`The 'laya-local' maker requires '${LAYA_DRIVER_PACKAGE}', an optional dependency installed only on macOS. On macOS, install it with 'bun add ${LAYA_DRIVER_PACKAGE}'.`, 'LAYA_BACKEND', { cause });
21
+ }
22
+ const record = loaded !== null && typeof loaded === 'object' ? loaded : {};
23
+ if (typeof record.createLayaDriver !== 'function') {
24
+ throw new DecisionConfigError(`Module '${LAYA_DRIVER_PACKAGE}' does not export createLayaDriver`, 'LAYA_BACKEND');
25
+ }
26
+ return record.createLayaDriver(options);
27
+ }
5
28
  const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
6
29
  /**
7
30
  * Registry mapping plain names to {@link DecisionMaker}s (task 0091, ADR-033
@@ -14,14 +37,34 @@ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
14
37
  */
15
38
  export class DecisionMakerRegistry {
16
39
  entries = new Map();
17
- /** Pre-registers `typesafe`, `fm-local` and `laya-local` unless `builtins: false`. */
40
+ /** Pre-registers `typesafe`, `fm-local`, `laya-local` and `llm-agent` unless `builtins: false`. */
18
41
  constructor(options) {
19
42
  if (options?.builtins === false)
20
43
  return;
21
44
  const { makerOptions = {}, driverOptions = {} } = options?.builtins ?? {};
22
- this.register('typesafe', () => createDecisionMaker({ backend: 'typesafe', ...makerOptions }));
45
+ // The driver lives here now (ADR-039), so the built-in no longer routes through the
46
+ // deprecated dynamic-import bridge. A missing key still throws inside this lazy factory,
47
+ // before any request, so the hub keeps reporting `reason: 'no-backend'`.
48
+ //
49
+ // `Omit<>` over an index-signature option bag collapses every declared field to `unknown`,
50
+ // so the bag is read back through its own type here — same object, same runtime values.
51
+ const transportOptions = makerOptions;
52
+ this.register('typesafe', () => createDecisionMaker({
53
+ driver: createTypesafeDriver({
54
+ apiKey: resolveApiKey(transportOptions),
55
+ baseURL: transportOptions.baseURL,
56
+ timeoutMs: transportOptions.timeoutMs,
57
+ maxRetries: transportOptions.maxRetries,
58
+ fetch: transportOptions.fetch,
59
+ }),
60
+ ...makerOptions,
61
+ }));
23
62
  this.register('fm-local', () => createDecisionMaker({ driver: createFmDriver(driverOptions['fm-local'] ?? {}), ...makerOptions }));
24
- this.register('laya-local', () => {
63
+ // Dynamic import: a top-level import would throw on hosts where the
64
+ // optional macOS package was not installed, and the rest of the hub
65
+ // would fail to load. A missing module becomes LAYA_BACKEND, which
66
+ // DecisionHub maps to reason 'no-backend'.
67
+ this.register('laya-local', async () => {
25
68
  const driverOpts = {
26
69
  ...driverOptions['laya-local'],
27
70
  };
@@ -29,10 +72,13 @@ export class DecisionMakerRegistry {
29
72
  driverOpts.env = makerOptions.env;
30
73
  }
31
74
  return createDecisionMaker({
32
- driver: createLayaDriver(driverOpts),
75
+ driver: await resolveBundledLayaDriver(driverOpts),
33
76
  ...makerOptions,
34
77
  });
35
78
  });
79
+ // Lazy like the rest: `names()`/`has()` construct nothing, and agent detection
80
+ // runs inside the first `ask`, so an unavailable agent surfaces as a fallback.
81
+ this.register('llm-agent', () => createDecisionMaker({ driver: createAgentDriver(driverOptions['llm-agent'] ?? {}), ...makerOptions }));
36
82
  }
37
83
  /** Register a maker or factory under `name`; chains. Throws on a bad or already-registered name. */
38
84
  register(name, source) {
package/dist/schema.d.ts CHANGED
@@ -18,8 +18,8 @@ declare const FullParamSchema: z.ZodObject<{
18
18
  string: "string";
19
19
  number: "number";
20
20
  boolean: "boolean";
21
- enum: "enum";
22
21
  json: "json";
22
+ enum: "enum";
23
23
  }>;
24
24
  default: z.ZodOptional<z.ZodUnknown>;
25
25
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -30,15 +30,15 @@ export declare const ParamSchema: z.ZodUnion<readonly [z.ZodEnum<{
30
30
  string: "string";
31
31
  number: "number";
32
32
  boolean: "boolean";
33
- enum: "enum";
34
33
  json: "json";
34
+ enum: "enum";
35
35
  }>, z.ZodObject<{
36
36
  type: z.ZodEnum<{
37
37
  string: "string";
38
38
  number: "number";
39
39
  boolean: "boolean";
40
- enum: "enum";
41
40
  json: "json";
41
+ enum: "enum";
42
42
  }>;
43
43
  default: z.ZodOptional<z.ZodUnknown>;
44
44
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -57,15 +57,15 @@ export declare const DecisionSchema: z.ZodObject<{
57
57
  string: "string";
58
58
  number: "number";
59
59
  boolean: "boolean";
60
- enum: "enum";
61
60
  json: "json";
61
+ enum: "enum";
62
62
  }>, z.ZodObject<{
63
63
  type: z.ZodEnum<{
64
64
  string: "string";
65
65
  number: "number";
66
66
  boolean: "boolean";
67
- enum: "enum";
68
67
  json: "json";
68
+ enum: "enum";
69
69
  }>;
70
70
  default: z.ZodOptional<z.ZodUnknown>;
71
71
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -77,7 +77,8 @@ export declare const DecisionSchema: z.ZodObject<{
77
77
  }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodType<Desc, unknown, z.core.$ZodTypeInternals<Desc, unknown>>>, z.ZodArray<z.ZodType<Desc, unknown, z.core.$ZodTypeInternals<Desc, unknown>>>]>>;
78
78
  fallback: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
79
79
  minConfidence: z.ZodOptional<z.ZodNumber>;
80
- maker: z.ZodOptional<z.ZodString>;
80
+ maker: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
81
+ shadow: z.ZodOptional<z.ZodString>;
81
82
  model: z.ZodOptional<z.ZodString>;
82
83
  }, z.core.$strict>;
83
84
  /** Whole-catalog schema (strict; `version` must be 1 and at least one decision is required). */
@@ -85,7 +86,7 @@ export declare const DecisionCatalogSchema: z.ZodObject<{
85
86
  $schema: z.ZodOptional<z.ZodString>;
86
87
  version: z.ZodLiteral<1>;
87
88
  defaults: z.ZodOptional<z.ZodObject<{
88
- maker: z.ZodOptional<z.ZodString>;
89
+ maker: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
89
90
  model: z.ZodOptional<z.ZodString>;
90
91
  minConfidence: z.ZodOptional<z.ZodNumber>;
91
92
  }, z.core.$strict>>;
@@ -101,15 +102,15 @@ export declare const DecisionCatalogSchema: z.ZodObject<{
101
102
  string: "string";
102
103
  number: "number";
103
104
  boolean: "boolean";
104
- enum: "enum";
105
105
  json: "json";
106
+ enum: "enum";
106
107
  }>, z.ZodObject<{
107
108
  type: z.ZodEnum<{
108
109
  string: "string";
109
110
  number: "number";
110
111
  boolean: "boolean";
111
- enum: "enum";
112
112
  json: "json";
113
+ enum: "enum";
113
114
  }>;
114
115
  default: z.ZodOptional<z.ZodUnknown>;
115
116
  values: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -121,7 +122,8 @@ export declare const DecisionCatalogSchema: z.ZodObject<{
121
122
  }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodType<Desc, unknown, z.core.$ZodTypeInternals<Desc, unknown>>>, z.ZodArray<z.ZodType<Desc, unknown, z.core.$ZodTypeInternals<Desc, unknown>>>]>>;
122
123
  fallback: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
123
124
  minConfidence: z.ZodOptional<z.ZodNumber>;
124
- maker: z.ZodOptional<z.ZodString>;
125
+ maker: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
126
+ shadow: z.ZodOptional<z.ZodString>;
125
127
  model: z.ZodOptional<z.ZodString>;
126
128
  }, z.core.$strict>>;
127
129
  }, z.core.$strict>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAQ,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;GAMG;AAEH,eAAO,MAAM,mBAAmB,QAAuB,CAAC;AACxD,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,QAAyB,CAAC;AACzD,kDAAkD;AAClD,eAAO,MAAM,kBAAkB,QAAsB,CAAC;AAoBtD,qFAAqF;AACrF,QAAA,MAAM,eAAe;;;;;;;;;;;kBAOR,CAAC;AAEd,kFAAkF;AAClF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;oBAA8C,CAAC;AAMvE,wGAAwG;AACxG,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAYd,CAAC;AAEd,gGAAgG;AAChG,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAiB5B,CAAC;AAEP,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAClE,qDAAqD;AACrD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAC5D,iEAAiE;AACjE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,6EAA6E;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAQ,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;GAMG;AAEH,eAAO,MAAM,mBAAmB,QAAuB,CAAC;AACxD,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,QAAyB,CAAC;AACzD,kDAAkD;AAClD,eAAO,MAAM,kBAAkB,QAAsB,CAAC;AAsCtD,qFAAqF;AACrF,QAAA,MAAM,eAAe;;;;;;;;;;;kBAOR,CAAC;AAEd,kFAAkF;AAClF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;oBAA8C,CAAC;AAMvE,wGAAwG;AACxG,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAad,CAAC;AAEd,gGAAgG;AAChG,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAiB5B,CAAC;AAEP,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAClE,qDAAqD;AACrD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAC5D,iEAAiE;AACjE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,6EAA6E;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC"}
package/dist/schema.js CHANGED
@@ -11,6 +11,23 @@ export const DECISION_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
11
11
  export const PARAM_NAME_PATTERN = /^[a-z][a-zA-Z0-9_]*$/;
12
12
  /** Maker registry names: lowercase kebab-case. */
13
13
  export const MAKER_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
14
+ /** One maker name, or an ordered fallback chain of distinct names (ADR-040). */
15
+ const MakerRefSchema = z.union([
16
+ z.string().regex(MAKER_NAME_PATTERN),
17
+ z
18
+ .array(z.string().regex(MAKER_NAME_PATTERN))
19
+ .min(1)
20
+ .superRefine((names, ctx) => {
21
+ const seen = new Set();
22
+ for (const name of names) {
23
+ if (seen.has(name)) {
24
+ ctx.addIssue({ code: 'custom', message: `maker list repeats '${name}'` });
25
+ return;
26
+ }
27
+ seen.add(name);
28
+ }
29
+ }),
30
+ ]);
14
31
  const JsonValueSchema = z.lazy(() => z.union([
15
32
  z.string(),
16
33
  z.number(),
@@ -46,7 +63,8 @@ export const DecisionSchema = z
46
63
  criteria: z.union([NoulCriteriaSchema, ChoiceCriteriaSchema, ScoreCriteriaSchema]).optional(),
47
64
  fallback: z.union([z.string(), z.number(), z.boolean()]),
48
65
  minConfidence: z.number().min(0).max(1).optional(),
49
- maker: z.string().regex(MAKER_NAME_PATTERN).optional(),
66
+ maker: MakerRefSchema.optional(),
67
+ shadow: z.string().regex(MAKER_NAME_PATTERN).optional(),
50
68
  model: z.string().optional(),
51
69
  })
52
70
  .strict();
@@ -57,7 +75,7 @@ export const DecisionCatalogSchema = z
57
75
  version: z.literal(1),
58
76
  defaults: z
59
77
  .object({
60
- maker: z.string().regex(MAKER_NAME_PATTERN).optional(),
78
+ maker: MakerRefSchema.optional(),
61
79
  model: z.string().optional(),
62
80
  minConfidence: z.number().min(0).max(1).optional(),
63
81
  })