@gobing-ai/ts-ai-runner 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/README.md +60 -5
- package/dist/decision/decision-maker.d.ts +22 -1
- package/dist/decision/decision-maker.d.ts.map +1 -1
- package/dist/decision/decision-maker.js +32 -10
- package/dist/decision/sampled-driver.d.ts +115 -0
- package/dist/decision/sampled-driver.d.ts.map +1 -0
- package/dist/decision/sampled-driver.js +365 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +4 -5
- package/src/decision/decision-maker.ts +53 -11
- package/src/decision/sampled-driver.ts +454 -0
- package/src/index.ts +1 -0
- package/dist/decision/typesafe-driver.d.ts +0 -24
- package/dist/decision/typesafe-driver.d.ts.map +0 -1
- package/dist/decision/typesafe-driver.js +0 -139
- package/src/decision/typesafe-driver.ts +0 -181
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine-agnostic sampled-driver core (task 0114 R1, ADR-038): prompt
|
|
3
|
+
* rendering, per-question enum options, answer parsing and frequency
|
|
4
|
+
* estimation, shared by every backend that answers by drawing k samples from a
|
|
5
|
+
* model. A backend supplies only the per-ask session hook.
|
|
6
|
+
*
|
|
7
|
+
* Pure by contract (R2): imports only the sibling `types` and `errors` modules —
|
|
8
|
+
* no SDK, no workspace package, no platform API. That is what lets a driver
|
|
9
|
+
* package outside `ts-ai-runner` build on it without a cycle.
|
|
10
|
+
*/
|
|
11
|
+
import { DecisionBackendError, DecisionRequestError } from './errors.js';
|
|
12
|
+
/**
|
|
13
|
+
* Fixed instructions text. It never asks for confidence, certainty or a
|
|
14
|
+
* probability — self-reported confidence would be fabricated calibration, which
|
|
15
|
+
* the decision contract forbids (ADR-030).
|
|
16
|
+
*/
|
|
17
|
+
export const DECISION_INSTRUCTIONS = 'You are a decision engine. Answer every question below using only the supplied state. ' +
|
|
18
|
+
'For each question choose exactly one of the listed options. ' +
|
|
19
|
+
'Reply with the JSON object the schema requires.';
|
|
20
|
+
/** Build the enum schema over the whole question map: one call answers every key. */
|
|
21
|
+
export function buildEnumSchema(questions) {
|
|
22
|
+
const properties = {};
|
|
23
|
+
for (const [key, question] of Object.entries(questions)) {
|
|
24
|
+
switch (question.kind) {
|
|
25
|
+
case 'choice':
|
|
26
|
+
properties[key] = { type: 'string', enum: Object.keys(question.labels) };
|
|
27
|
+
break;
|
|
28
|
+
case 'score':
|
|
29
|
+
properties[key] = { type: 'string', enum: question.rubric.map((_, level) => String(level)) };
|
|
30
|
+
break;
|
|
31
|
+
case 'noul':
|
|
32
|
+
properties[key] = { type: 'string', enum: ['yes', 'no'] };
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const keys = Object.keys(questions);
|
|
37
|
+
return {
|
|
38
|
+
type: 'object',
|
|
39
|
+
title: 'Decision',
|
|
40
|
+
'x-order': keys,
|
|
41
|
+
required: keys,
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
properties,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Format a {@link Desc} into prompt text: strings verbatim, structured values JSON, absence empty. */
|
|
47
|
+
export function formatDesc(desc) {
|
|
48
|
+
if (typeof desc === 'string')
|
|
49
|
+
return desc;
|
|
50
|
+
if (desc === null || desc === undefined)
|
|
51
|
+
return '';
|
|
52
|
+
return JSON.stringify(desc);
|
|
53
|
+
}
|
|
54
|
+
/** The declared option values of a question, in declaration order (estimation + validation share this). */
|
|
55
|
+
export function declaredOptions(question) {
|
|
56
|
+
switch (question.kind) {
|
|
57
|
+
case 'choice':
|
|
58
|
+
return Object.keys(question.labels);
|
|
59
|
+
case 'score':
|
|
60
|
+
return question.rubric.map((_, level) => String(level));
|
|
61
|
+
case 'noul':
|
|
62
|
+
return ['yes', 'no'];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render the positional prompt: the state as text (JSON when structured), then
|
|
67
|
+
* one block per question key with the prompt `Desc` and its options with
|
|
68
|
+
* descriptions — choice label keys, score level indices with the rubric text,
|
|
69
|
+
* noul `yes`/`no` with any outcome descriptions.
|
|
70
|
+
*/
|
|
71
|
+
export function buildPrompt(state, questions) {
|
|
72
|
+
const parts = [];
|
|
73
|
+
const stateText = typeof state === 'string' ? state : state === null || state === undefined ? '' : JSON.stringify(state);
|
|
74
|
+
if (stateText.length > 0)
|
|
75
|
+
parts.push(stateText);
|
|
76
|
+
for (const [key, question] of Object.entries(questions)) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
const prompt = formatDesc(question.prompt);
|
|
79
|
+
lines.push(`Question "${key}":${prompt.length > 0 ? ` ${prompt}` : ''}`);
|
|
80
|
+
lines.push('Options:');
|
|
81
|
+
switch (question.kind) {
|
|
82
|
+
case 'choice':
|
|
83
|
+
for (const [label, desc] of Object.entries(question.labels)) {
|
|
84
|
+
const d = formatDesc(desc);
|
|
85
|
+
lines.push(d.length > 0 ? `- ${label}: ${d}` : `- ${label}`);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'score':
|
|
89
|
+
question.rubric.forEach((desc, level) => {
|
|
90
|
+
const d = formatDesc(desc);
|
|
91
|
+
lines.push(d.length > 0 ? `- ${level}: ${d}` : `- ${level}`);
|
|
92
|
+
});
|
|
93
|
+
break;
|
|
94
|
+
case 'noul': {
|
|
95
|
+
for (const outcome of ['yes', 'no']) {
|
|
96
|
+
const d = formatDesc(question[outcome]);
|
|
97
|
+
lines.push(d.length > 0 ? `- ${outcome}: ${d}` : `- ${outcome}`);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
parts.push(lines.join('\n'));
|
|
103
|
+
}
|
|
104
|
+
return parts.join('\n\n');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Laya reference `confidence_from_probs` on the empirical distribution:
|
|
108
|
+
* `1 − H(p) / ln(n)` over the n declared options. One-hot ⇒ 1, uniform ⇒ 0.
|
|
109
|
+
*/
|
|
110
|
+
export function entropyConfidence(probabilities) {
|
|
111
|
+
const n = probabilities.length;
|
|
112
|
+
if (n <= 1)
|
|
113
|
+
return 1;
|
|
114
|
+
let entropy = 0;
|
|
115
|
+
for (const p of probabilities) {
|
|
116
|
+
if (p > 0)
|
|
117
|
+
entropy -= p * Math.log(p);
|
|
118
|
+
}
|
|
119
|
+
return 1 - entropy / Math.log(n);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Choice frequencies over every supplied label (unsampled ⇒ 0). The answer
|
|
123
|
+
* label is the most frequent with ties broken by label declaration order.
|
|
124
|
+
*/
|
|
125
|
+
export function estimateChoice(labels, samples) {
|
|
126
|
+
const counts = new Map(labels.map((label) => [label, 0]));
|
|
127
|
+
for (const sample of samples)
|
|
128
|
+
counts.set(sample, (counts.get(sample) ?? 0) + 1);
|
|
129
|
+
const k = samples.length;
|
|
130
|
+
const probabilities = {};
|
|
131
|
+
const distribution = [];
|
|
132
|
+
let bestLabel = labels[0] ?? '';
|
|
133
|
+
let bestCount = -1;
|
|
134
|
+
for (const label of labels) {
|
|
135
|
+
const count = counts.get(label) ?? 0;
|
|
136
|
+
const probability = count / k;
|
|
137
|
+
distribution.push(probability);
|
|
138
|
+
probabilities[label] = probability;
|
|
139
|
+
// Declaration order wins ties: strict `>` keeps the earlier label.
|
|
140
|
+
if (count > bestCount) {
|
|
141
|
+
bestCount = count;
|
|
142
|
+
bestLabel = label;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
kind: 'choice',
|
|
147
|
+
label: bestLabel,
|
|
148
|
+
confidence: entropyConfidence(distribution),
|
|
149
|
+
probabilities,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Score frequencies over the level indices `0…n-1`. The answer score is the
|
|
154
|
+
* most frequent level with ties going to the lower level; the legend maps each
|
|
155
|
+
* index back to its rubric description.
|
|
156
|
+
*/
|
|
157
|
+
export function estimateScore(rubric, samples) {
|
|
158
|
+
const k = samples.length;
|
|
159
|
+
const counts = [];
|
|
160
|
+
for (const sample of samples) {
|
|
161
|
+
const level = Number.parseInt(sample, 10);
|
|
162
|
+
counts[level] = (counts[level] ?? 0) + 1;
|
|
163
|
+
}
|
|
164
|
+
const probabilities = {};
|
|
165
|
+
const distribution = [];
|
|
166
|
+
let bestLevel = 0;
|
|
167
|
+
let bestCount = -1;
|
|
168
|
+
for (let level = 0; level < rubric.length; level++) {
|
|
169
|
+
const count = counts[level] ?? 0;
|
|
170
|
+
const probability = count / k;
|
|
171
|
+
distribution.push(probability);
|
|
172
|
+
probabilities[level] = probability;
|
|
173
|
+
// Lower level wins ties: strict `>` keeps the smaller index.
|
|
174
|
+
if (count > bestCount) {
|
|
175
|
+
bestCount = count;
|
|
176
|
+
bestLevel = level;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const legend = {};
|
|
180
|
+
rubric.forEach((desc, level) => {
|
|
181
|
+
legend[level] = desc;
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
kind: 'score',
|
|
185
|
+
score: bestLevel,
|
|
186
|
+
confidence: entropyConfidence(distribution),
|
|
187
|
+
legend,
|
|
188
|
+
probabilities,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** Noul is a bare yes-probability — no `confidence` key, by contract. */
|
|
192
|
+
export function estimateNoul(samples) {
|
|
193
|
+
const yes = samples.filter((sample) => sample === 'yes').length;
|
|
194
|
+
return { kind: 'noul', probability: yes / samples.length };
|
|
195
|
+
}
|
|
196
|
+
/** A parsed value outside the declared enum is a schema violation, never an answer. */
|
|
197
|
+
export function requireEnumValue(parsed, key, allowed, label = 'sample') {
|
|
198
|
+
const value = parsed[key];
|
|
199
|
+
if (value === undefined || !allowed.includes(value)) {
|
|
200
|
+
throw new DecisionBackendError(`${label} sample for '${key}' violates the schema enum: ${value === undefined ? '(missing)' : `'${value}'`}`, undefined);
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
/** Parse text as JSON, reporting failure instead of throwing. */
|
|
205
|
+
function tryParse(text) {
|
|
206
|
+
try {
|
|
207
|
+
return { ok: true, value: JSON.parse(text) };
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { ok: false };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The first balanced `{…}` substring, scanned with a string/escape-aware brace
|
|
215
|
+
* counter so braces inside string values do not close the object early.
|
|
216
|
+
*/
|
|
217
|
+
function firstBalancedObject(text) {
|
|
218
|
+
for (let start = text.indexOf('{'); start !== -1; start = text.indexOf('{', start + 1)) {
|
|
219
|
+
let depth = 0;
|
|
220
|
+
let inString = false;
|
|
221
|
+
let escaped = false;
|
|
222
|
+
for (let i = start; i < text.length; i++) {
|
|
223
|
+
const ch = text[i];
|
|
224
|
+
if (inString) {
|
|
225
|
+
if (escaped)
|
|
226
|
+
escaped = false;
|
|
227
|
+
else if (ch === '\\')
|
|
228
|
+
escaped = true;
|
|
229
|
+
else if (ch === '"')
|
|
230
|
+
inString = false;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (ch === '"') {
|
|
234
|
+
inString = true;
|
|
235
|
+
}
|
|
236
|
+
else if (ch === '{') {
|
|
237
|
+
depth += 1;
|
|
238
|
+
}
|
|
239
|
+
else if (ch === '}') {
|
|
240
|
+
depth -= 1;
|
|
241
|
+
if (depth === 0) {
|
|
242
|
+
const parsed = tryParse(text.slice(start, i + 1));
|
|
243
|
+
if (parsed.ok)
|
|
244
|
+
return parsed.value;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Lenient answer parser for models without constrained decoding (task 0114 R6).
|
|
254
|
+
* Tries the whole text, then the first fenced block, then the first balanced
|
|
255
|
+
* object in prose. The result must be a flat object of string values.
|
|
256
|
+
*/
|
|
257
|
+
export function extractJsonAnswer(text) {
|
|
258
|
+
const direct = tryParse(text.trim());
|
|
259
|
+
let value = direct.ok ? direct.value : undefined;
|
|
260
|
+
if (value === undefined) {
|
|
261
|
+
const fence = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
262
|
+
if (fence !== null) {
|
|
263
|
+
const fenced = tryParse((fence[1] ?? '').trim());
|
|
264
|
+
if (fenced.ok)
|
|
265
|
+
value = fenced.value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (value === undefined)
|
|
269
|
+
value = firstBalancedObject(text);
|
|
270
|
+
if (value === undefined) {
|
|
271
|
+
throw new DecisionBackendError('no JSON object in sample output', undefined);
|
|
272
|
+
}
|
|
273
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
274
|
+
throw new DecisionBackendError(`sample output is not a JSON object: ${JSON.stringify(value)}`, undefined);
|
|
275
|
+
}
|
|
276
|
+
const record = {};
|
|
277
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
278
|
+
if (typeof entry !== 'string') {
|
|
279
|
+
throw new DecisionBackendError(`sample output has a non-string value for '${key}'`, undefined);
|
|
280
|
+
}
|
|
281
|
+
record[key] = entry;
|
|
282
|
+
}
|
|
283
|
+
return record;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Build a sampled driver: k draws through one per-ask {@link SampleSession},
|
|
287
|
+
* enum-checked against the declared options and reduced to sample frequencies.
|
|
288
|
+
*
|
|
289
|
+
* The order inside `ask` is the contract (R5): validate → render → open →
|
|
290
|
+
* sample → enum-check → estimate, with `close` in `finally` either way.
|
|
291
|
+
*/
|
|
292
|
+
export function createSampledDriver(options) {
|
|
293
|
+
const { name, deterministic = false, sampleLabel } = options;
|
|
294
|
+
const samples = options.samples ?? 5;
|
|
295
|
+
const concurrency = options.concurrency ?? 1;
|
|
296
|
+
if (!Number.isInteger(samples) || samples < 1) {
|
|
297
|
+
throw new DecisionRequestError(`samples must be a positive integer; got ${samples}`, undefined, undefined);
|
|
298
|
+
}
|
|
299
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
300
|
+
throw new DecisionRequestError(`concurrency must be a positive integer; got ${concurrency}`, undefined, undefined);
|
|
301
|
+
}
|
|
302
|
+
const k = deterministic ? 1 : samples;
|
|
303
|
+
const label = sampleLabel ?? name;
|
|
304
|
+
return {
|
|
305
|
+
name,
|
|
306
|
+
estimator: { kind: 'sample-frequency', samples: k, greedy: deterministic },
|
|
307
|
+
async ask({ state, questions, model, }) {
|
|
308
|
+
const entries = Object.entries(questions);
|
|
309
|
+
if (entries.length === 0) {
|
|
310
|
+
throw new DecisionRequestError('expected a nonempty question map', undefined, undefined);
|
|
311
|
+
}
|
|
312
|
+
for (const [key, question] of entries) {
|
|
313
|
+
const optionCount = declaredOptions(question).length;
|
|
314
|
+
if (optionCount < 2) {
|
|
315
|
+
throw new DecisionRequestError(`question '${key}' has fewer than two options (${optionCount})`, undefined, undefined);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const instructions = options.instructions ?? DECISION_INSTRUCTIONS;
|
|
319
|
+
const prompt = buildPrompt(state, questions);
|
|
320
|
+
const session = await options.open({ instructions, prompt, questions, state, model });
|
|
321
|
+
try {
|
|
322
|
+
const draws = [];
|
|
323
|
+
for (let start = 0; start < k; start += concurrency) {
|
|
324
|
+
const size = Math.min(concurrency, k - start);
|
|
325
|
+
const batch = Array.from({ length: size }, () => session.sample());
|
|
326
|
+
// allSettled awaits the whole batch before close; the first
|
|
327
|
+
// rejection in draw order wins, so no partial estimate escapes.
|
|
328
|
+
const settled = await Promise.allSettled(batch);
|
|
329
|
+
const failure = settled.find((result) => result.status === 'rejected');
|
|
330
|
+
if (failure !== undefined)
|
|
331
|
+
throw failure.reason;
|
|
332
|
+
for (const result of settled) {
|
|
333
|
+
draws.push(result.value);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const allowed = new Map(entries.map(([key, question]) => [key, declaredOptions(question)]));
|
|
337
|
+
const samplesByKey = new Map(entries.map(([key]) => [key, []]));
|
|
338
|
+
for (const draw of draws) {
|
|
339
|
+
for (const [key] of entries) {
|
|
340
|
+
samplesByKey.get(key)?.push(requireEnumValue(draw, key, allowed.get(key) ?? [], label));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const answers = {};
|
|
344
|
+
for (const [key, question] of entries) {
|
|
345
|
+
const values = samplesByKey.get(key) ?? [];
|
|
346
|
+
switch (question.kind) {
|
|
347
|
+
case 'choice':
|
|
348
|
+
answers[key] = estimateChoice(Object.keys(question.labels), values);
|
|
349
|
+
break;
|
|
350
|
+
case 'score':
|
|
351
|
+
answers[key] = estimateScore(question.rubric, values);
|
|
352
|
+
break;
|
|
353
|
+
case 'noul':
|
|
354
|
+
answers[key] = estimateNoul(values);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return answers;
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
await session.close?.();
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './agents/shims';
|
|
|
5
5
|
export * from './ai-runner';
|
|
6
6
|
export * from './decision/decision-maker';
|
|
7
7
|
export * from './decision/errors';
|
|
8
|
+
export * from './decision/sampled-driver';
|
|
8
9
|
export * from './decision/types';
|
|
9
10
|
export * from './decision/validation';
|
|
10
11
|
export * from './doctor-runner';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from './agents/shims.js';
|
|
|
5
5
|
export * from './ai-runner.js';
|
|
6
6
|
export * from './decision/decision-maker.js';
|
|
7
7
|
export * from './decision/errors.js';
|
|
8
|
+
export * from './decision/sampled-driver.js';
|
|
8
9
|
export * from './decision/types.js';
|
|
9
10
|
export * from './decision/validation.js';
|
|
10
11
|
export * from './doctor-runner.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-ai-runner",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.33",
|
|
4
4
|
"description": "@gobing-ai/ts-ai-runner — Coding-agent shims, detection, doctor checks, and prompt execution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -47,13 +47,12 @@
|
|
|
47
47
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-ai-runner-v<version> && git push --tags' && exit 1"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@gobing-ai/ts-infra": "^0.5.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
52
|
-
"@typesafe-ai/sdk": "0.6.0"
|
|
50
|
+
"@gobing-ai/ts-infra": "^0.5.33",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.5.33"
|
|
53
52
|
},
|
|
54
53
|
"devDependencies": {
|
|
55
54
|
"@types/bun": "1.4.3",
|
|
56
|
-
"@gobing-ai/ts-db": "^0.5.
|
|
55
|
+
"@gobing-ai/ts-db": "^0.5.33"
|
|
57
56
|
},
|
|
58
57
|
"publishConfig": {
|
|
59
58
|
"access": "public"
|
|
@@ -17,7 +17,6 @@ import type {
|
|
|
17
17
|
ScoreAnswer,
|
|
18
18
|
} from './types';
|
|
19
19
|
import { q } from './types';
|
|
20
|
-
import { createTypesafeDriver } from './typesafe-driver';
|
|
21
20
|
import { validateAnswers, validateQuestions } from './validation';
|
|
22
21
|
|
|
23
22
|
/** Public decision surface: batch `ask` plus the three single-question sugar methods. */
|
|
@@ -55,7 +54,14 @@ export type DecisionBackend = 'typesafe' | 'laya-local' | 'fm-local';
|
|
|
55
54
|
export interface DecisionMakerOptions {
|
|
56
55
|
/** Custom driver — when supplied, `backend` is ignored, no default driver is constructed and no key is required. */
|
|
57
56
|
driver?: DecisionDriver;
|
|
58
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Named backend selector. Default: `'typesafe'`. Resolution order: `driver` → `backend` → `'typesafe'`.
|
|
59
|
+
*
|
|
60
|
+
* @deprecated `'typesafe'` (including the implicit default) now loads its driver from
|
|
61
|
+
* `@gobing-ai/ts-ai-decision` through a dynamic import, so it requires that package to be
|
|
62
|
+
* installed. Prefer `driver: createTypesafeDriver(...)` imported from `@gobing-ai/ts-ai-decision`;
|
|
63
|
+
* this bridge is removed in the next minor (ADR-039).
|
|
64
|
+
*/
|
|
59
65
|
backend?: DecisionBackend;
|
|
60
66
|
/** Injected env record; defaults to `getProcessEnv()` (doctor-runner convention). */
|
|
61
67
|
env?: Record<string, string | undefined>;
|
|
@@ -75,7 +81,7 @@ export interface DecisionMakerOptions {
|
|
|
75
81
|
* Resolve the key per R7: `options.apiKey ?? (options.env ?? getProcessEnv()).TYPESAFE_API_KEY`.
|
|
76
82
|
* Throws before any driver construction or request.
|
|
77
83
|
*/
|
|
78
|
-
function resolveApiKey(options: DecisionMakerOptions): string {
|
|
84
|
+
export function resolveApiKey(options: DecisionMakerOptions): string {
|
|
79
85
|
const env = options.env ?? getProcessEnv();
|
|
80
86
|
const apiKey = options.apiKey ?? env.TYPESAFE_API_KEY;
|
|
81
87
|
if (!apiKey) {
|
|
@@ -122,6 +128,49 @@ export async function resolveLayaDriver(
|
|
|
122
128
|
return mod.createLayaDriver(options);
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
const TYPESAFE_DRIVER_PACKAGE = '@gobing-ai/ts-ai-decision';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve the `typesafe` driver from `@gobing-ai/ts-ai-decision` (task 0116 R5, ADR-039).
|
|
135
|
+
*
|
|
136
|
+
* The driver moved out of this package, which is now the contract-only surface. The key is
|
|
137
|
+
* resolved FIRST so a missing key keeps its exact pre-move error and no import is attempted.
|
|
138
|
+
*
|
|
139
|
+
* @internal Exported for the branch tests; see {@link resolveLayaDriver}.
|
|
140
|
+
*/
|
|
141
|
+
export async function resolveTypesafeDriver(
|
|
142
|
+
options: DecisionMakerOptions,
|
|
143
|
+
importModule: (specifier: string) => Promise<unknown> = (specifier) => import(specifier),
|
|
144
|
+
): Promise<DecisionDriver> {
|
|
145
|
+
const apiKey = resolveApiKey(options);
|
|
146
|
+
let mod: { createTypesafeDriver?: (opts?: unknown) => DecisionDriver };
|
|
147
|
+
try {
|
|
148
|
+
mod = (await importModule(TYPESAFE_DRIVER_PACKAGE)) as {
|
|
149
|
+
createTypesafeDriver?: (opts?: unknown) => DecisionDriver;
|
|
150
|
+
};
|
|
151
|
+
} catch (cause) {
|
|
152
|
+
throw new DecisionConfigError(
|
|
153
|
+
`The 'typesafe' backend requires '${TYPESAFE_DRIVER_PACKAGE}' to be installed; install it with 'bun add ${TYPESAFE_DRIVER_PACKAGE}'`,
|
|
154
|
+
'TYPESAFE_BACKEND',
|
|
155
|
+
{ cause },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (typeof mod.createTypesafeDriver !== 'function') {
|
|
159
|
+
throw new DecisionConfigError(
|
|
160
|
+
`Module '${TYPESAFE_DRIVER_PACKAGE}' does not export createTypesafeDriver`,
|
|
161
|
+
'TYPESAFE_BACKEND',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return mod.createTypesafeDriver({
|
|
165
|
+
apiKey,
|
|
166
|
+
model: options.model,
|
|
167
|
+
baseURL: options.baseURL,
|
|
168
|
+
timeoutMs: options.timeoutMs,
|
|
169
|
+
maxRetries: options.maxRetries,
|
|
170
|
+
fetch: options.fetch,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
125
174
|
const FM_DRIVER_PACKAGE = '@gobing-ai/ts-decision-fm';
|
|
126
175
|
|
|
127
176
|
/** @internal Exported for task 0086 R9 branch tests; see {@link resolveLayaDriver}. */
|
|
@@ -159,14 +208,7 @@ export function createDecisionMaker(options: DecisionMakerOptions = {}): Decisio
|
|
|
159
208
|
if (resolvedDriver) return resolvedDriver;
|
|
160
209
|
const backend = options.backend ?? 'typesafe';
|
|
161
210
|
if (backend === 'typesafe') {
|
|
162
|
-
resolvedDriver =
|
|
163
|
-
apiKey: resolveApiKey(options),
|
|
164
|
-
model: options.model,
|
|
165
|
-
baseURL: options.baseURL,
|
|
166
|
-
timeoutMs: options.timeoutMs,
|
|
167
|
-
maxRetries: options.maxRetries,
|
|
168
|
-
fetch: options.fetch,
|
|
169
|
-
});
|
|
211
|
+
resolvedDriver = await resolveTypesafeDriver(options);
|
|
170
212
|
return resolvedDriver;
|
|
171
213
|
}
|
|
172
214
|
if (backend === 'laya-local') {
|