@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/README.md +208 -42
- package/dist/agent-driver.d.ts +59 -0
- package/dist/agent-driver.d.ts.map +1 -0
- package/dist/agent-driver.js +104 -0
- package/dist/catalog.js +1 -0
- package/dist/hub.d.ts +72 -10
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js +163 -28
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/registry.d.ts +43 -6
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +52 -6
- package/dist/schema.d.ts +12 -10
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +20 -2
- package/dist/types.d.ts +11 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/typesafe-driver.d.ts +24 -0
- package/dist/typesafe-driver.d.ts.map +1 -0
- package/dist/typesafe-driver.js +160 -0
- package/package.json +9 -5
- package/schemas/decision-catalog.schema.json +26 -2
- package/src/agent-driver.ts +164 -0
- package/src/catalog.ts +1 -0
- package/src/hub.ts +293 -45
- package/src/index.ts +2 -0
- package/src/registry.ts +111 -10
- package/src/schema.ts +21 -2
- package/src/types.ts +12 -3
- package/src/typesafe-driver.ts +212 -0
package/dist/types.d.ts
CHANGED
|
@@ -46,13 +46,21 @@ export interface DecisionDefinition {
|
|
|
46
46
|
readonly fallback: string | number | boolean;
|
|
47
47
|
/** Overrides `defaults.minConfidence` (0..1). */
|
|
48
48
|
readonly minConfidence?: number;
|
|
49
|
-
/** Registry name, checked at hub load, not by the
|
|
50
|
-
readonly maker?:
|
|
49
|
+
/** Registry name, or an ordered fallback chain of names (ADR-040); checked at hub load, not by the loader. */
|
|
50
|
+
readonly maker?: MakerRef;
|
|
51
|
+
/** One maker asked beside the primary on live traffic and reported, never served (ADR-040). */
|
|
52
|
+
readonly shadow?: string;
|
|
51
53
|
readonly model?: string;
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* A maker reference: one registry name, or an ordered fallback chain of names (ADR-040). The
|
|
57
|
+
* authored form is preserved (`string` stays `string`), so a single-name catalog round-trips
|
|
58
|
+
* byte-identically; the hub resolves the chain per call.
|
|
59
|
+
*/
|
|
60
|
+
export type MakerRef = string | readonly string[];
|
|
53
61
|
/** Catalog-wide maker/model/minConfidence defaults; every key optional. */
|
|
54
62
|
export interface CatalogDefaults {
|
|
55
|
-
readonly maker?:
|
|
63
|
+
readonly maker?: MakerRef;
|
|
56
64
|
readonly model?: string;
|
|
57
65
|
readonly minConfidence?: number;
|
|
58
66
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAE1D,gEAAgE;AAChE,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAEvD,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC;IACxB,wDAAwD;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,2GAA2G;AAC3G,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC;AAE5B,+DAA+D;AAC/D,MAAM,MAAM,gBAAgB,GACtB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;CAAE,GAChF;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAA;CAAE,GAChE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAA;CAAE,CAAC;AAE7G,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,mDAAmD;IACnD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,yFAAyF;IACzF,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAC7C,iDAAiD;IACjD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TypeSafe backend driver — the only decision file that imports
|
|
3
|
+
* `@typesafe-ai/sdk` (0069 pin). Client wiring (R1/R2), neutral⇄SDK question
|
|
4
|
+
* and answer mapping (R4/R5), and the error translation table (R7) live here;
|
|
5
|
+
* everything above this file stays vendor-neutral.
|
|
6
|
+
*/
|
|
7
|
+
import { type DecisionDriver } from '@gobing-ai/ts-ai-runner';
|
|
8
|
+
/** Configuration the facade resolves (key per R7) and forwards to the TypeSafe driver. */
|
|
9
|
+
export interface TypesafeDriverConfig {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
baseURL?: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build the TypeSafe driver: exactly one `TypeSafeClient` per driver instance
|
|
19
|
+
* (R2), the API key always passed explicitly so the SDK's `TYPESAFE_API_KEY`
|
|
20
|
+
* self-resolution never runs (R1). All transport failures — at construction
|
|
21
|
+
* or at ask time — come back as `DecisionError`s, never SDK classes (R7).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createTypesafeDriver(config: TypesafeDriverConfig): DecisionDriver;
|
|
24
|
+
//# sourceMappingURL=typesafe-driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typesafe-driver.d.ts","sourceRoot":"","sources":["../src/typesafe-driver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAKH,KAAK,cAAc,EAQtB,MAAM,yBAAyB,CAAC;AAqBjC,0FAA0F;AAC1F,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAqDjF"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TypeSafe backend driver — the only decision file that imports
|
|
3
|
+
* `@typesafe-ai/sdk` (0069 pin). Client wiring (R1/R2), neutral⇄SDK question
|
|
4
|
+
* and answer mapping (R4/R5), and the error translation table (R7) live here;
|
|
5
|
+
* everything above this file stays vendor-neutral.
|
|
6
|
+
*/
|
|
7
|
+
import { DecisionAuthError, DecisionBackendError, DecisionConnectionError, DecisionRateLimitError, DecisionRequestError, DecisionTimeoutError, validateAnswers, validateQuestions, } from '@gobing-ai/ts-ai-runner';
|
|
8
|
+
import { APIConnectionError, APIError, APITimeoutError, AuthenticationError, choice, PermissionDeniedError, RateLimitError, score, TypeSafeClient, TypeSafeError, } from '@typesafe-ai/sdk';
|
|
9
|
+
/**
|
|
10
|
+
* Build the TypeSafe driver: exactly one `TypeSafeClient` per driver instance
|
|
11
|
+
* (R2), the API key always passed explicitly so the SDK's `TYPESAFE_API_KEY`
|
|
12
|
+
* self-resolution never runs (R1). All transport failures — at construction
|
|
13
|
+
* or at ask time — come back as `DecisionError`s, never SDK classes (R7).
|
|
14
|
+
*/
|
|
15
|
+
export function createTypesafeDriver(config) {
|
|
16
|
+
let client;
|
|
17
|
+
try {
|
|
18
|
+
client = new TypeSafeClient({
|
|
19
|
+
apiKey: config.apiKey,
|
|
20
|
+
// baseURL is forwarded as-is. SDK 0.7.0 still exports no default
|
|
21
|
+
// base-URL constant (`ENV.baseURL` is the env-var NAME, and the
|
|
22
|
+
// documented default `https://api.typesafe.ai` lives only in the
|
|
23
|
+
// config JSDoc), so an omitted baseURL keeps letting the SDK
|
|
24
|
+
// self-resolve `TYPESAFE_BASE_URL` — documented residual for 0073.
|
|
25
|
+
baseURL: config.baseURL,
|
|
26
|
+
defaultModel: config.model,
|
|
27
|
+
timeout: config.timeoutMs,
|
|
28
|
+
retry: config.maxRetries === undefined ? undefined : { maxRetries: config.maxRetries },
|
|
29
|
+
fetch: config.fetch,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
translateError(err);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
name: 'typesafe',
|
|
37
|
+
async ask({ state, questions, model }) {
|
|
38
|
+
validateQuestions(questions);
|
|
39
|
+
// The neutral contract allows `null` state ("no caller context") and the SDK type
|
|
40
|
+
// accepts it, but the wire drops a null field and the API answers 422 "state: Field
|
|
41
|
+
// required" — so every fully parameterless decision used to fall back on this backend.
|
|
42
|
+
// Same null→'' mapping the laya and fm drivers already apply; structured state passes
|
|
43
|
+
// through untouched (the wire accepts an object or array).
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
const sdkQuestions = Object.fromEntries(Object.entries(questions).map(([name, question]) => [name, toSdkQuestion(question)]));
|
|
47
|
+
result = await client.systemOne({ state: state ?? '', questions: sdkQuestions, model });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
translateError(err);
|
|
51
|
+
}
|
|
52
|
+
// R6: the SDK echoes the request's question names, so the mapped
|
|
53
|
+
// record keeps the caller's keys in correspondence. R8: `model`
|
|
54
|
+
// and `usage` on the result are deliberately not surfaced.
|
|
55
|
+
if (!result?.answers || typeof result.answers !== 'object' || Array.isArray(result.answers)) {
|
|
56
|
+
throw new DecisionBackendError('Invalid decision response: expected answers map', undefined);
|
|
57
|
+
}
|
|
58
|
+
const answers = Object.fromEntries(Object.entries(result.answers).map(([name, answer]) => [name, fromSdkAnswer(answer)]));
|
|
59
|
+
validateAnswers(questions, answers);
|
|
60
|
+
return answers;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Neutral question → SDK wire question (R4). `null` is "undescribed" on the wire. */
|
|
65
|
+
function toSdkQuestion(question) {
|
|
66
|
+
switch (question.kind) {
|
|
67
|
+
case 'choice':
|
|
68
|
+
return choice(question.prompt ?? null, question.labels);
|
|
69
|
+
case 'score':
|
|
70
|
+
return score(question.prompt ?? null, question.rubric);
|
|
71
|
+
case 'noul': {
|
|
72
|
+
const outcomes = question.yes === undefined && question.no === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: { true: question.yes, false: question.no };
|
|
75
|
+
// SDK 0.7.0's `noul()` overloads cannot express an undescribed noul
|
|
76
|
+
// (`instructions` null with no criteria), which the neutral contract allows,
|
|
77
|
+
// so the same object is built directly. The intent type is identical to the
|
|
78
|
+
// builder's, and the wire shape is unchanged for every described case.
|
|
79
|
+
const noulQuestion = { type: 'noul', instructions: question.prompt ?? null };
|
|
80
|
+
if (outcomes !== undefined)
|
|
81
|
+
noulQuestion.criteria = outcomes;
|
|
82
|
+
return noulQuestion;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the rubric legend as the neutral index → description map. The SDK's `ScoreLegend`
|
|
88
|
+
* threads a recursive conditional (`SentContent`) that the compiler cannot resolve once the
|
|
89
|
+
* question map is the neutral union, so the value is handed over here; `validateAnswers`
|
|
90
|
+
* enforces the real shape at runtime.
|
|
91
|
+
*/
|
|
92
|
+
function legendOf(wire) {
|
|
93
|
+
return wire.legend;
|
|
94
|
+
}
|
|
95
|
+
/** SDK wire response → neutral answer (R5). Noul gets no confidence — the wire has none and none is invented. */
|
|
96
|
+
function fromSdkAnswer(answer) {
|
|
97
|
+
if (!answer || typeof answer !== 'object')
|
|
98
|
+
throw new DecisionBackendError('Invalid decision response: expected answer object', undefined);
|
|
99
|
+
switch (answer.type) {
|
|
100
|
+
case 'choice':
|
|
101
|
+
return {
|
|
102
|
+
kind: 'choice',
|
|
103
|
+
label: answer.choice,
|
|
104
|
+
confidence: answer.confidence,
|
|
105
|
+
probabilities: answer.probabilities,
|
|
106
|
+
};
|
|
107
|
+
case 'score': {
|
|
108
|
+
// SDK 0.7.0 infers `ScoreResponse<ScoreCriteria>` from `ResultFor<Question>` too deeply to
|
|
109
|
+
// instantiate (TS2589), so the branch reads through the non-generic default instead and
|
|
110
|
+
// hands the legend over through a helper. `validateAnswers` checks the values at runtime.
|
|
111
|
+
const wire = answer;
|
|
112
|
+
return {
|
|
113
|
+
kind: 'score',
|
|
114
|
+
score: wire.score,
|
|
115
|
+
confidence: wire.confidence,
|
|
116
|
+
legend: legendOf(wire),
|
|
117
|
+
probabilities: wire.probabilities,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case 'noul':
|
|
121
|
+
return { kind: 'noul', probability: answer.noul };
|
|
122
|
+
default:
|
|
123
|
+
throw new DecisionBackendError('Invalid decision response: unknown answer kind', undefined);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* R7, per the design doc's error table. Ordered subclass-before-base:
|
|
128
|
+
* `APITimeoutError` extends `APIConnectionError`, and the HTTP subclasses
|
|
129
|
+
* extend `APIError`. Rows the table does not name fall to the nearest home:
|
|
130
|
+
* 4xx (`NotFoundError` included) is a `DecisionRequestError`, and any local
|
|
131
|
+
* `TypeSafeError` (empty questions, invalid client config) lands there too
|
|
132
|
+
* with `status: undefined` — so no SDK class ever escapes. Foreign errors are
|
|
133
|
+
* rethrown untouched; they are not this package's to translate.
|
|
134
|
+
*/
|
|
135
|
+
function translateError(err) {
|
|
136
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
+
if (err instanceof RateLimitError) {
|
|
138
|
+
throw new DecisionRateLimitError(message, err.status, err.retryAfterMs, { cause: err });
|
|
139
|
+
}
|
|
140
|
+
if (err instanceof AuthenticationError || err instanceof PermissionDeniedError) {
|
|
141
|
+
throw new DecisionAuthError(message, err.status, { cause: err });
|
|
142
|
+
}
|
|
143
|
+
if (err instanceof APITimeoutError) {
|
|
144
|
+
throw new DecisionTimeoutError(message, err.timeoutMs, { cause: err });
|
|
145
|
+
}
|
|
146
|
+
if (err instanceof APIConnectionError) {
|
|
147
|
+
throw new DecisionConnectionError(message, { cause: err });
|
|
148
|
+
}
|
|
149
|
+
if (err instanceof APIError) {
|
|
150
|
+
if (err.status < 500) {
|
|
151
|
+
const raw = typeof err.body === 'string' ? err.body : (JSON.stringify(err.body) ?? '');
|
|
152
|
+
throw new DecisionRequestError(message, err.status, raw.slice(0, 200) || undefined, { cause: err });
|
|
153
|
+
}
|
|
154
|
+
throw new DecisionBackendError(message, err.status, { cause: err });
|
|
155
|
+
}
|
|
156
|
+
if (err instanceof TypeSafeError) {
|
|
157
|
+
throw new DecisionRequestError(message, undefined, message, { cause: err });
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-ai-decision",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.33",
|
|
4
4
|
"description": "@gobing-ai/ts-ai-decision — Declarative YAML decision catalog over the ts-ai-runner DecisionMaker surface.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -51,13 +51,17 @@
|
|
|
51
51
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-ai-decision-v<version> && git push --tags' && exit 1"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@gobing-ai/ts-ai-runner": "^0.5.
|
|
55
|
-
"@gobing-ai/ts-decision-fm": "^0.5.
|
|
56
|
-
"@gobing-ai/ts-
|
|
57
|
-
"@
|
|
54
|
+
"@gobing-ai/ts-ai-runner": "^0.5.33",
|
|
55
|
+
"@gobing-ai/ts-decision-fm": "^0.5.33",
|
|
56
|
+
"@gobing-ai/ts-runtime": "^0.5.33",
|
|
57
|
+
"@typesafe-ai/sdk": "0.7.0",
|
|
58
58
|
"zod": "^4.1.0"
|
|
59
59
|
},
|
|
60
|
+
"optionalDependencies": {
|
|
61
|
+
"@gobing-ai/ts-laya-mlx": "^0.5.33"
|
|
62
|
+
},
|
|
60
63
|
"devDependencies": {
|
|
64
|
+
"@gobing-ai/ts-utils": "^0.5.33",
|
|
61
65
|
"@types/bun": "1.4.3"
|
|
62
66
|
},
|
|
63
67
|
"publishConfig": {
|
|
@@ -18,9 +18,21 @@
|
|
|
18
18
|
"additionalProperties": false,
|
|
19
19
|
"properties": {
|
|
20
20
|
"maker": {
|
|
21
|
+
"oneOf": [
|
|
22
|
+
{ "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
23
|
+
{
|
|
24
|
+
"type": "array",
|
|
25
|
+
"minItems": 1,
|
|
26
|
+
"uniqueItems": true,
|
|
27
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"description": "Registered maker name, or an ordered fallback list (ADR-040). Built-ins: typesafe | fm-local | laya-local | llm-agent."
|
|
31
|
+
},
|
|
32
|
+
"shadow": {
|
|
21
33
|
"type": "string",
|
|
22
34
|
"pattern": "^[a-z][a-z0-9-]*$",
|
|
23
|
-
"description": "
|
|
35
|
+
"description": "Maker asked beside the primary and reported on the result, never served (ADR-040)."
|
|
24
36
|
},
|
|
25
37
|
"model": {
|
|
26
38
|
"type": "string",
|
|
@@ -213,9 +225,21 @@
|
|
|
213
225
|
"maximum": 1
|
|
214
226
|
},
|
|
215
227
|
"maker": {
|
|
228
|
+
"oneOf": [
|
|
229
|
+
{ "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
230
|
+
{
|
|
231
|
+
"type": "array",
|
|
232
|
+
"minItems": 1,
|
|
233
|
+
"uniqueItems": true,
|
|
234
|
+
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
"description": "Registered maker name, or an ordered fallback list (ADR-040); checked by the hub, not the loader."
|
|
238
|
+
},
|
|
239
|
+
"shadow": {
|
|
216
240
|
"type": "string",
|
|
217
241
|
"pattern": "^[a-z][a-z0-9-]*$",
|
|
218
|
-
"description": "
|
|
242
|
+
"description": "Maker asked beside the primary and reported on the result, never served (ADR-040)."
|
|
219
243
|
},
|
|
220
244
|
"model": {
|
|
221
245
|
"type": "string"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `llm-agent` decision driver (task 0115, ADR-038/ADR-039): any installed
|
|
3
|
+
* `ts-ai-runner` coding agent or model answering through the shared sampled-driver
|
|
4
|
+
* core. Each sample is one isolated one-shot prompt run; probabilities are sample
|
|
5
|
+
* frequencies (ADR-030) — the agent is never asked for a confidence.
|
|
6
|
+
*
|
|
7
|
+
* Every sample runs in a per-ask scratch directory. That is load-bearing, not
|
|
8
|
+
* tidiness: the claude one-shot shim grants `--permission-mode acceptEdits
|
|
9
|
+
* --allowedTools Write Edit`, so a decision must never be able to write into the
|
|
10
|
+
* caller's workspace (R6).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AgentDetector,
|
|
15
|
+
type AgentName,
|
|
16
|
+
AiRunner,
|
|
17
|
+
createSampledDriver,
|
|
18
|
+
DecisionBackendError,
|
|
19
|
+
DecisionConfigError,
|
|
20
|
+
DecisionTimeoutError,
|
|
21
|
+
declaredOptions,
|
|
22
|
+
extractJsonAnswer,
|
|
23
|
+
type PromptOptions,
|
|
24
|
+
type Question,
|
|
25
|
+
type SampledDecisionDriver,
|
|
26
|
+
type SampleSession,
|
|
27
|
+
TIER1_PRIORITY,
|
|
28
|
+
} from '@gobing-ai/ts-ai-runner';
|
|
29
|
+
import { createNodeFileSystem, type FileSystem, getProcessEnv, joinPath } from '@gobing-ai/ts-runtime';
|
|
30
|
+
|
|
31
|
+
/** The one runner method a sampled decision needs; a real `AiRunner` satisfies it. */
|
|
32
|
+
export type AgentPromptRunner = Pick<AiRunner, 'runPromptCommand'>;
|
|
33
|
+
|
|
34
|
+
/** The one detector method the driver needs; a real `AgentDetector` satisfies it. */
|
|
35
|
+
export type AgentDetectorPort = Pick<AgentDetector, 'detectOne'>;
|
|
36
|
+
|
|
37
|
+
/** Configuration options for {@link createAgentDriver}. */
|
|
38
|
+
export interface AgentDriverOptions {
|
|
39
|
+
/** Agent to sample. Default: the first installed agent in `TIER1_PRIORITY` (fm is never auto-picked). */
|
|
40
|
+
readonly agent?: AgentName;
|
|
41
|
+
/** Model id forwarded as `PromptOptions.model`; a per-ask `model` overrides it. */
|
|
42
|
+
readonly model?: string;
|
|
43
|
+
/** Samples per ask. Default: 3. Ignored when `deterministic`. */
|
|
44
|
+
readonly samples?: number;
|
|
45
|
+
/** One greedy sample; probabilities are one-hot. Default: false. */
|
|
46
|
+
readonly deterministic?: boolean;
|
|
47
|
+
/** Max samples in flight. Default: 1 (sequential). */
|
|
48
|
+
readonly concurrency?: number;
|
|
49
|
+
/** Budget for one sample. Default: 120_000. */
|
|
50
|
+
readonly timeoutMs?: number;
|
|
51
|
+
/** Injected runner. Default: a lazy `new AiRunner()`. */
|
|
52
|
+
readonly runner?: AgentPromptRunner;
|
|
53
|
+
/** Injected detector. Default: a lazy `new AgentDetector({ runner })`. */
|
|
54
|
+
readonly detector?: AgentDetectorPort;
|
|
55
|
+
/** Scratch-directory lifecycle. Default: ts-runtime's node FileSystem. */
|
|
56
|
+
readonly fs?: Pick<FileSystem, 'ensureDir' | 'deleteFile'>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The agent driver: a sampled decision driver named `llm-agent`. */
|
|
60
|
+
export interface AgentDecisionDriver extends SampledDecisionDriver {
|
|
61
|
+
readonly name: 'llm-agent';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Instructions for a *generic* agent. fm's text mentions "the schema" because fm
|
|
66
|
+
* receives `--schema`; agents get no such flag, so this text never does.
|
|
67
|
+
*/
|
|
68
|
+
export const AGENT_INSTRUCTIONS =
|
|
69
|
+
'You are a decision function. Read the context and questions, then reply with exactly one JSON ' +
|
|
70
|
+
'object mapping each question key to one of its allowed option strings. No prose, no code, no ' +
|
|
71
|
+
'tool use, no confidence values.';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The per-ask reply contract appended to the prompt: every key with its allowed
|
|
75
|
+
* option strings, plus one example object. It never asks for probabilities or
|
|
76
|
+
* confidence (ADR-030).
|
|
77
|
+
*/
|
|
78
|
+
export function JSON_REPLY_SUFFIX(questions: Record<string, Question>): string {
|
|
79
|
+
const lines = ['', 'Reply with exactly one JSON object and nothing else.'];
|
|
80
|
+
for (const [key, question] of Object.entries(questions)) {
|
|
81
|
+
const options = declaredOptions(question)
|
|
82
|
+
.map((option) => `"${option}"`)
|
|
83
|
+
.join(', ');
|
|
84
|
+
lines.push(`- "${key}": one of ${options}`);
|
|
85
|
+
}
|
|
86
|
+
const example: Record<string, string> = {};
|
|
87
|
+
for (const [key, question] of Object.entries(questions)) {
|
|
88
|
+
example[key] = declaredOptions(question)[0] ?? '';
|
|
89
|
+
}
|
|
90
|
+
lines.push('', 'Example:', JSON.stringify(example));
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build the `llm-agent` decision driver. Construction spawns nothing: the runner
|
|
98
|
+
* and detector are created lazily and the agent is resolved on the first `ask`.
|
|
99
|
+
*/
|
|
100
|
+
export function createAgentDriver(options: AgentDriverOptions = {}): AgentDecisionDriver {
|
|
101
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
102
|
+
const fs = options.fs ?? createNodeFileSystem();
|
|
103
|
+
// Created inside the factory, and only when not injected, so construction never spawns.
|
|
104
|
+
const runner = options.runner ?? new AiRunner();
|
|
105
|
+
// A real AiRunner satisfies AgentPromptRunner, so this cast only widens a fake back
|
|
106
|
+
// to the injected seam; a fake runner is always paired with its own fake detector.
|
|
107
|
+
const detector = options.detector ?? new AgentDetector({ runner: runner as AiRunner });
|
|
108
|
+
// Memoised on success only: a later ask retries an unresolved agent after an install.
|
|
109
|
+
let resolved: AgentName | undefined;
|
|
110
|
+
|
|
111
|
+
async function resolveAgent(): Promise<AgentName> {
|
|
112
|
+
if (options.agent !== undefined) return options.agent;
|
|
113
|
+
if (resolved !== undefined) return resolved;
|
|
114
|
+
for (const candidate of TIER1_PRIORITY) {
|
|
115
|
+
const detected = await detector.detectOne(candidate);
|
|
116
|
+
if (detected.installed) {
|
|
117
|
+
resolved = detected.name as AgentName;
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw new DecisionConfigError(
|
|
122
|
+
`no coding agent available for llm-agent; install one of: ${TIER1_PRIORITY.join(', ')}`,
|
|
123
|
+
'AGENT',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return createSampledDriver({
|
|
128
|
+
name: 'llm-agent',
|
|
129
|
+
samples: options.samples ?? 3,
|
|
130
|
+
deterministic: options.deterministic ?? false,
|
|
131
|
+
concurrency: options.concurrency ?? 1,
|
|
132
|
+
instructions: AGENT_INSTRUCTIONS,
|
|
133
|
+
async open({ instructions, prompt, questions, model }): Promise<SampleSession> {
|
|
134
|
+
const agent = await resolveAgent();
|
|
135
|
+
const scratch = joinPath(getProcessEnv().TMPDIR ?? '/tmp', `llm-agent-${crypto.randomUUID()}`);
|
|
136
|
+
await fs.ensureDir(scratch);
|
|
137
|
+
const input = `${instructions}\n\n${prompt}${JSON_REPLY_SUFFIX(questions)}`;
|
|
138
|
+
const promptOptions: PromptOptions = { input, mode: 'text' };
|
|
139
|
+
const effectiveModel = model ?? options.model;
|
|
140
|
+
if (effectiveModel !== undefined) promptOptions.model = effectiveModel;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
async sample(): Promise<Record<string, string>> {
|
|
144
|
+
const result = await runner.runPromptCommand(agent, promptOptions, {
|
|
145
|
+
cwd: scratch,
|
|
146
|
+
timeout: timeoutMs,
|
|
147
|
+
});
|
|
148
|
+
if (result.exitCode === null) {
|
|
149
|
+
throw new DecisionTimeoutError(`llm-agent sample timed out after ${timeoutMs}ms`, timeoutMs);
|
|
150
|
+
}
|
|
151
|
+
if (result.exitCode !== 0) {
|
|
152
|
+
const firstLine = result.stderr.split('\n')[0]?.trim() ?? '';
|
|
153
|
+
throw new DecisionBackendError(
|
|
154
|
+
`llm-agent sample failed (${agent} exit ${result.exitCode}): ${firstLine}`,
|
|
155
|
+
undefined,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return extractJsonAnswer(result.stdout);
|
|
159
|
+
},
|
|
160
|
+
close: () => fs.deleteFile(scratch),
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
}) as AgentDecisionDriver;
|
|
164
|
+
}
|
package/src/catalog.ts
CHANGED
|
@@ -354,6 +354,7 @@ function checkDecision(id: string, raw: ParsedDecision, source: string): Decisio
|
|
|
354
354
|
fallback: raw.fallback,
|
|
355
355
|
...(raw.minConfidence !== undefined ? { minConfidence: raw.minConfidence } : {}),
|
|
356
356
|
...(raw.maker !== undefined ? { maker: raw.maker } : {}),
|
|
357
|
+
...(raw.shadow !== undefined ? { shadow: raw.shadow } : {}),
|
|
357
358
|
...(raw.model !== undefined ? { model: raw.model } : {}),
|
|
358
359
|
};
|
|
359
360
|
}
|