@gobing-ai/ts-ai-runner 0.4.69 → 0.5.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.
package/README.md CHANGED
@@ -32,6 +32,7 @@ bun add @gobing-ai/ts-ai-runner
32
32
  | `AgentEvents` / `AiRunnerProcessEvents` | Typed event maps for agent and process-level observability |
33
33
  | `AGENT_SHIMS` / `TIER1_PRIORITY` / `TIER2_AGENTS` / `DISPLAY_ORDER` | Agent registry constants |
34
34
  | `isAgentName()` | Type guard for supported agent identifiers |
35
+ | `createDecisionMaker()` / `q` | Batch `ask` and single-question `choice`/`score`/`noul` decisions with provider-neutral question/answer types and a `DecisionError` taxonomy |
35
36
 
36
37
  Supported agent identifiers: `claude`, `codex`, `gemini` (deprecated), `pi`, `omp`, `opencode`, `antigravity-cli`, `openclaw`, `hermes`, `grok`, `deepseek`. The `antigravity` id is a deprecated alias of `antigravity-cli`. See [Deprecation & Aliases](#deprecation--aliases).
37
38
 
@@ -730,6 +731,119 @@ If the agent supports an auth-status command, `getAuthCommand()` already returns
730
731
 
731
732
  After these changes, the new agent is automatically available to `AiRunner`, `AgentDetector`, `DoctorRunner`, `TeamOrchestrator`, and all downstream consumers — no further registration needed.
732
733
 
734
+ ## Decision Making
735
+
736
+ `createDecisionMaker()` exposes a provider-neutral LLM decision surface: ask N questions against one
737
+ shared state and get typed, discriminated answers back. Questions are built with the `q` builders;
738
+ answers are decoded into neutral `ChoiceAnswer` / `ScoreAnswer` / `NoulAnswer` types — no SDK type
739
+ crosses this boundary.
740
+
741
+ ### Batch `ask` — many questions, one request
742
+
743
+ All questions in one `ask` call share the state and cost a single backend request:
744
+
745
+ ```ts
746
+ import { createDecisionMaker, q } from '@gobing-ai/ts-ai-runner';
747
+
748
+ const decisions = createDecisionMaker(); // reads TYPESAFE_API_KEY from the environment
749
+ const state = { ticket: 'T-1042', body: 'Users cannot reset their passwords.' };
750
+
751
+ const answers = await decisions.ask({
752
+ state,
753
+ questions: {
754
+ route: q.choice('Which team should own this?', {
755
+ billing: 'Invoices and payment issues',
756
+ access: 'Login and account access',
757
+ other: 'Anything else',
758
+ }),
759
+ urgency: q.score('How urgent is this?', ['Routine', 'Elevated', 'Drop everything']),
760
+ duplicate: q.noul('Has this ticket been reported before?'),
761
+ },
762
+ });
763
+
764
+ answers.route.label; // 'billing' | 'access' | 'other'
765
+ answers.urgency.score; // rubric level
766
+ answers.duplicate.probability; // yes-probability
767
+ ```
768
+
769
+ ### Single-question sugar
770
+
771
+ `choice`, `score`, and `noul` are one-question convenience forms over `ask`:
772
+
773
+ ```ts
774
+ const route = await decisions.choice(state, 'Which team should own this?', {
775
+ billing: 'Invoices and payment issues',
776
+ access: 'Login and account access',
777
+ });
778
+ route.label; // 'billing' | 'access'
779
+
780
+ const duplicate = await decisions.noul(state, 'Has this ticket been reported before?');
781
+ duplicate.probability; // yes-probability — no `confidence` field: the API reports none for yes/no
782
+ ```
783
+
784
+ A yes/no answer carries only `probability`. The API returns no calibration confidence for noul
785
+ questions, so none is synthesized.
786
+
787
+ ### Configuration
788
+
789
+ The API key resolves as `options.apiKey`, else `TYPESAFE_API_KEY` from the injected `env` record,
790
+ else from the process environment. A missing key throws `DecisionConfigError` before any request:
791
+
792
+ ```ts
793
+ const key = 'sk-…'; // from your secret store
794
+ const explicit = createDecisionMaker({ apiKey: key }); // explicit key wins
795
+ const sandboxed = createDecisionMaker({ env: { TYPESAFE_API_KEY: key } }); // injected record — the host owns the environment
796
+ ```
797
+
798
+ Other options: `model`, `baseURL`, `timeoutMs`, `maxRetries`, and an injected `fetch` for tests. When
799
+ `baseURL` is omitted the SDK resolves `TYPESAFE_BASE_URL` from the environment itself — pass `baseURL`
800
+ explicitly to pin the endpoint.
801
+
802
+ ### Errors
803
+
804
+ SDK failures and malformed questions/responses surface as `DecisionError` subclasses. Custom
805
+ drivers' own exceptions propagate; no vendor SDK error class escapes the default driver:
806
+
807
+ | Error | When | Carries |
808
+ | ----- | ------- | ------- |
809
+ | `DecisionConfigError` | No API key resolved | variable name |
810
+ | `DecisionAuthError` | Authentication or permission rejected upstream | HTTP status |
811
+ | `DecisionRateLimitError` | Rate limited upstream | status, `retryAfterMs` |
812
+ | `DecisionTimeoutError` | Request timed out | `timeoutMs` |
813
+ | `DecisionConnectionError` | Backend unreachable | underlying cause |
814
+ | `DecisionRequestError` | Invalid question or request rejected as malformed (other 4xx) | status, body summary |
815
+ | `DecisionBackendError` | Invalid answer or upstream server failure (5xx) | HTTP status when present |
816
+
817
+ The facade validates both default and custom drivers: answer names/kinds must match the questions,
818
+ choice labels must belong to the supplied label map, probability/confidence values must be finite
819
+ and within `[0, 1]`, and score values must fit the zero-indexed rubric (fractional expected scores
820
+ are supported). Missing probability/legend keys are rejected. These checks establish structural
821
+ validity, not calibration or decision quality.
822
+
823
+ Applications own decision policy and evidence selection. For example, Spur can adapt its existing
824
+ `HitlResponder` to use DecisionMaker while retaining its own HITL actions and fallback behavior;
825
+ the workflow engine itself has no dependency on ai-runner.
826
+
827
+ ### Adding a backend driver
828
+
829
+ Additional backend drivers are the intended extension point. A driver implements only `ask` —
830
+ question building, the single-question sugar, and answer typing all come from the facade:
831
+
832
+ ```ts
833
+ import type { Answer, DecisionDriver } from '@gobing-ai/ts-ai-runner';
834
+
835
+ const myDriver: DecisionDriver = {
836
+ name: 'my-backend',
837
+ async ask() {
838
+ const answers: Record<string, Answer> = {};
839
+ // Call your backend; return one answer per key in the question map.
840
+ return answers;
841
+ },
842
+ };
843
+
844
+ const custom = createDecisionMaker({ driver: myDriver }); // no TYPESAFE_API_KEY required
845
+ ```
846
+
733
847
  ## Boundary Notes
734
848
 
735
849
  - This package is a command adapter, not an agent orchestration framework.
@@ -0,0 +1,45 @@
1
+ import type { AnswersFor, ChoiceAnswer, DecisionDriver, DecisionState, Desc, NoulAnswer, Question, ScoreAnswer } from './types';
2
+ /** Public decision surface: batch `ask` plus the three single-question sugar methods. */
3
+ export interface DecisionMaker {
4
+ /** Name of the backing driver (e.g. `"typesafe"`, or a custom driver's name). */
5
+ readonly driver: string;
6
+ /** Evaluate N named questions against one shared state in a single driver request. */
7
+ ask<const Q extends Record<string, Question>>(req: {
8
+ state: DecisionState;
9
+ questions: Q;
10
+ model?: string;
11
+ }): Promise<AnswersFor<Q>>;
12
+ /** Pick one label. Sugar over `ask` with exactly one choice question. */
13
+ choice<const L extends string>(state: DecisionState, prompt: Desc, labels: Record<L, Desc>): Promise<ChoiceAnswer<L>>;
14
+ /** Score against a rubric. Sugar over `ask` with exactly one score question. */
15
+ score(state: DecisionState, prompt: Desc, rubric: readonly [Desc, Desc, ...Desc[]]): Promise<ScoreAnswer>;
16
+ /** Yes/no judgment. Sugar over `ask` with exactly one noul question. */
17
+ noul(state: DecisionState, prompt?: Desc, outcomes?: {
18
+ yes?: Desc;
19
+ no?: Desc;
20
+ }): Promise<NoulAnswer>;
21
+ }
22
+ /** Factory options: driver selection, credential injection, and transport tuning. */
23
+ export interface DecisionMakerOptions {
24
+ /** Custom driver — when supplied, the default TypeSafe driver is never constructed and no key is required. */
25
+ driver?: DecisionDriver;
26
+ /** Injected env record; defaults to `getProcessEnv()` (doctor-runner convention). */
27
+ env?: Record<string, string | undefined>;
28
+ /** Explicit key — wins over `env.TYPESAFE_API_KEY`. */
29
+ apiKey?: string;
30
+ /** Forwarded to the TypeSafe driver; default is the SDK's model default. */
31
+ model?: string;
32
+ baseURL?: string;
33
+ timeoutMs?: number;
34
+ maxRetries?: number;
35
+ /** Injected fetch for tests. */
36
+ fetch?: typeof fetch;
37
+ }
38
+ /**
39
+ * Build a DecisionMaker. `options.driver` wins when supplied; otherwise the
40
+ * TypeSafe driver is constructed lazily on first use, after key resolution —
41
+ * so a custom driver never requires a key and a missing key fails before any
42
+ * request (R6/R7).
43
+ */
44
+ export declare function createDecisionMaker(options?: DecisionMakerOptions): DecisionMaker;
45
+ //# sourceMappingURL=decision-maker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision-maker.d.ts","sourceRoot":"","sources":["../../src/decision/decision-maker.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACR,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,IAAI,EACJ,UAAU,EACV,QAAQ,EACR,WAAW,EACd,MAAM,SAAS,CAAC;AAKjB,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE;QAC/C,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,CAAC,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3B,yEAAyE;IACzE,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EACzB,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,IAAI,EACZ,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,GACxB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,gFAAgF;IAChF,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1G,wEAAwE;IACxE,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,IAAI,CAAC;QAAC,EAAE,CAAC,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACxG;AAED,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACjC,8GAA8G;IAC9G,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAkBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CA6CrF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The public decision surface — the only type callers see. `choice`, `score`,
3
+ * and `noul` are implemented once, here, as `ask()` with a single-entry
4
+ * question map; drivers implement `ask` and nothing else, so the sugar can
5
+ * never drift from the batch path. This file imports no SDK (R9).
6
+ */
7
+ import { getProcessEnv } from '@gobing-ai/ts-runtime';
8
+ import { DecisionConfigError } from './errors.js';
9
+ import { q } from './types.js';
10
+ import { createTypesafeDriver } from './typesafe-driver.js';
11
+ import { validateAnswers, validateQuestions } from './validation.js';
12
+ /**
13
+ * Resolve the key per R7: `options.apiKey ?? (options.env ?? getProcessEnv()).TYPESAFE_API_KEY`.
14
+ * Throws before any driver construction or request.
15
+ */
16
+ function resolveApiKey(options) {
17
+ const env = options.env ?? getProcessEnv();
18
+ const apiKey = options.apiKey ?? env.TYPESAFE_API_KEY;
19
+ if (!apiKey) {
20
+ throw new DecisionConfigError('Missing TYPESAFE_API_KEY — set it in the environment or pass options.apiKey.', 'TYPESAFE_API_KEY');
21
+ }
22
+ return apiKey;
23
+ }
24
+ /**
25
+ * Build a DecisionMaker. `options.driver` wins when supplied; otherwise the
26
+ * TypeSafe driver is constructed lazily on first use, after key resolution —
27
+ * so a custom driver never requires a key and a missing key fails before any
28
+ * request (R6/R7).
29
+ */
30
+ export function createDecisionMaker(options = {}) {
31
+ // Lazy driver slot: resolved on first use, at most once. A caller-supplied
32
+ // driver never reaches key resolution or default-driver construction (R6).
33
+ let defaultDriver;
34
+ const resolveDriver = () => {
35
+ if (options.driver)
36
+ return options.driver;
37
+ defaultDriver ??= createTypesafeDriver({
38
+ apiKey: resolveApiKey(options),
39
+ model: options.model,
40
+ baseURL: options.baseURL,
41
+ timeoutMs: options.timeoutMs,
42
+ maxRetries: options.maxRetries,
43
+ fetch: options.fetch,
44
+ });
45
+ return defaultDriver;
46
+ };
47
+ const ask = async (req) => {
48
+ // R3: the questions map reaches the driver untouched — same reference,
49
+ // no reordering, renaming, or dropping.
50
+ const driver = resolveDriver();
51
+ validateQuestions(req.questions);
52
+ const answers = await driver.ask(req);
53
+ validateAnswers(req.questions, answers);
54
+ // Runtime validation above establishes each question/answer correspondence.
55
+ return answers;
56
+ };
57
+ return {
58
+ driver: options.driver?.name ?? 'typesafe',
59
+ ask,
60
+ // R4/R5: each sugar method is one `ask` with exactly one `q.*` question,
61
+ // unwrapping the single answer — no second driver call, no duplicated
62
+ // request assembly.
63
+ choice: (state, prompt, labels) => ask({ state, questions: { question: q.choice(prompt, labels) } }).then((a) => a.question),
64
+ score: (state, prompt, rubric) => ask({ state, questions: { question: q.score(prompt, rubric) } }).then((a) => a.question),
65
+ noul: (state, prompt, outcomes) => ask({ state, questions: { question: q.noul(prompt, outcomes) } }).then((a) => a.question),
66
+ };
67
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `DecisionError` taxonomy. Both the facade and the driver throw from this set;
3
+ * no raw SDK error class escapes the package. Keeping every class in one file
4
+ * makes that invariant checkable by reading a single file.
5
+ */
6
+ /** Base class for every decision-surface error. */
7
+ export declare class DecisionError extends Error {
8
+ constructor(message: string, options?: {
9
+ cause?: unknown;
10
+ });
11
+ }
12
+ /** Missing configuration — e.g. no `TYPESAFE_API_KEY` in the injected env. Carries the variable name. */
13
+ export declare class DecisionConfigError extends DecisionError {
14
+ readonly variable: string;
15
+ constructor(message: string, variable: string, options?: {
16
+ cause?: unknown;
17
+ });
18
+ }
19
+ /** Authentication or permission failure upstream. Carries the HTTP status. */
20
+ export declare class DecisionAuthError extends DecisionError {
21
+ readonly status: number | undefined;
22
+ constructor(message: string, status: number | undefined, options?: {
23
+ cause?: unknown;
24
+ });
25
+ }
26
+ /** Rate limited upstream. Carries the status and the parsed retry-after hint. */
27
+ export declare class DecisionRateLimitError extends DecisionError {
28
+ readonly status: number | undefined;
29
+ readonly retryAfterMs: number | undefined;
30
+ constructor(message: string, status: number | undefined, retryAfterMs: number | undefined, options?: {
31
+ cause?: unknown;
32
+ });
33
+ }
34
+ /** Request timed out. Carries the configured timeout in milliseconds. */
35
+ export declare class DecisionTimeoutError extends DecisionError {
36
+ readonly timeoutMs: number | undefined;
37
+ constructor(message: string, timeoutMs: number | undefined, options?: {
38
+ cause?: unknown;
39
+ });
40
+ }
41
+ /** The backend was unreachable. Carries the underlying cause. */
42
+ export declare class DecisionConnectionError extends DecisionError {
43
+ constructor(message: string, options?: {
44
+ cause?: unknown;
45
+ });
46
+ }
47
+ /** The request was rejected as malformed (4xx other than auth/rate-limit). Carries status and a body summary. */
48
+ export declare class DecisionRequestError extends DecisionError {
49
+ readonly status: number | undefined;
50
+ readonly bodySummary: string | undefined;
51
+ constructor(message: string, status: number | undefined, bodySummary: string | undefined, options?: {
52
+ cause?: unknown;
53
+ });
54
+ }
55
+ /** Upstream server failure (5xx). Carries the HTTP status. */
56
+ export declare class DecisionBackendError extends DecisionError {
57
+ readonly status: number | undefined;
58
+ constructor(message: string, status: number | undefined, options?: {
59
+ cause?: unknown;
60
+ });
61
+ }
62
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/decision/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,mDAAmD;AACnD,qBAAa,aAAc,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI7D;AAED,yGAAyG;AACzG,qBAAa,mBAAoB,SAAQ,aAAa;IAG9C,QAAQ,CAAC,QAAQ,EAAE,MAAM;gBADzB,OAAO,EAAE,MAAM,EACN,QAAQ,EAAE,MAAM,EACzB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIpC;AAED,8EAA8E;AAC9E,qBAAa,iBAAkB,SAAQ,aAAa;IAG5C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS;gBADnC,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,GAAG,SAAS,EACnC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIpC;AAED,iFAAiF;AACjF,qBAAa,sBAAuB,SAAQ,aAAa;IAGjD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS;IACnC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS;gBAFzC,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,EACzC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIpC;AAED,yEAAyE;AACzE,qBAAa,oBAAqB,SAAQ,aAAa;IAG/C,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS;gBADtC,OAAO,EAAE,MAAM,EACN,SAAS,EAAE,MAAM,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIpC;AAED,iEAAiE;AACjE,qBAAa,uBAAwB,SAAQ,aAAa;gBAC1C,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAG7D;AAED,iHAAiH;AACjH,qBAAa,oBAAqB,SAAQ,aAAa;IAG/C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS;gBAFxC,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,WAAW,EAAE,MAAM,GAAG,SAAS,EACxC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIpC;AAED,8DAA8D;AAC9D,qBAAa,oBAAqB,SAAQ,aAAa;IAG/C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS;gBADnC,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,GAAG,SAAS,EACnC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIpC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `DecisionError` taxonomy. Both the facade and the driver throw from this set;
3
+ * no raw SDK error class escapes the package. Keeping every class in one file
4
+ * makes that invariant checkable by reading a single file.
5
+ */
6
+ /** Base class for every decision-surface error. */
7
+ export class DecisionError extends Error {
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = new.target.name;
11
+ }
12
+ }
13
+ /** Missing configuration — e.g. no `TYPESAFE_API_KEY` in the injected env. Carries the variable name. */
14
+ export class DecisionConfigError extends DecisionError {
15
+ variable;
16
+ constructor(message, variable, options) {
17
+ super(message, options);
18
+ this.variable = variable;
19
+ }
20
+ }
21
+ /** Authentication or permission failure upstream. Carries the HTTP status. */
22
+ export class DecisionAuthError extends DecisionError {
23
+ status;
24
+ constructor(message, status, options) {
25
+ super(message, options);
26
+ this.status = status;
27
+ }
28
+ }
29
+ /** Rate limited upstream. Carries the status and the parsed retry-after hint. */
30
+ export class DecisionRateLimitError extends DecisionError {
31
+ status;
32
+ retryAfterMs;
33
+ constructor(message, status, retryAfterMs, options) {
34
+ super(message, options);
35
+ this.status = status;
36
+ this.retryAfterMs = retryAfterMs;
37
+ }
38
+ }
39
+ /** Request timed out. Carries the configured timeout in milliseconds. */
40
+ export class DecisionTimeoutError extends DecisionError {
41
+ timeoutMs;
42
+ constructor(message, timeoutMs, options) {
43
+ super(message, options);
44
+ this.timeoutMs = timeoutMs;
45
+ }
46
+ }
47
+ /** The backend was unreachable. Carries the underlying cause. */
48
+ export class DecisionConnectionError extends DecisionError {
49
+ constructor(message, options) {
50
+ super(message, options);
51
+ }
52
+ }
53
+ /** The request was rejected as malformed (4xx other than auth/rate-limit). Carries status and a body summary. */
54
+ export class DecisionRequestError extends DecisionError {
55
+ status;
56
+ bodySummary;
57
+ constructor(message, status, bodySummary, options) {
58
+ super(message, options);
59
+ this.status = status;
60
+ this.bodySummary = bodySummary;
61
+ }
62
+ }
63
+ /** Upstream server failure (5xx). Carries the HTTP status. */
64
+ export class DecisionBackendError extends DecisionError {
65
+ status;
66
+ constructor(message, status, options) {
67
+ super(message, options);
68
+ this.status = status;
69
+ }
70
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Provider-neutral decision vocabulary — deliberately not re-exported from
3
+ * `@typesafe-ai/sdk`. Re-exporting the SDK's response types would bind every
4
+ * caller to the vendor; the neutral names here (`kind`, `labels`, `rubric`,
5
+ * `probability`) keep vendor mapping explicit and confined to the driver.
6
+ * This file imports nothing — no SDK, no workspace package (boundary rule).
7
+ */
8
+ /** Any JSON-serializable value. */
9
+ export type Json = string | number | boolean | null | Json[] | {
10
+ [k: string]: Json;
11
+ };
12
+ /** Caller-supplied decision context: free text, a structured object, or a list. */
13
+ export type DecisionState = string | Json[] | {
14
+ [k: string]: Json;
15
+ } | null;
16
+ /** A description; `null` means "left undescribed" (e.g. a label needing no rationale). */
17
+ export type Desc = string | Json[] | {
18
+ [k: string]: Json;
19
+ } | null;
20
+ /** Pick one label from a fixed set. Labels are the answer's type parameter. */
21
+ export type ChoiceQuestion<L extends string> = {
22
+ kind: 'choice';
23
+ prompt?: Desc;
24
+ labels: Record<L, Desc>;
25
+ };
26
+ /** Score against a rubric; at least two levels, enforced at compile time. */
27
+ export type ScoreQuestion = {
28
+ kind: 'score';
29
+ prompt?: Desc;
30
+ rubric: readonly [Desc, Desc, ...Desc[]];
31
+ };
32
+ /** Binary yes/no judgment with optional per-outcome descriptions. */
33
+ export type NoulQuestion = {
34
+ kind: 'noul';
35
+ prompt?: Desc;
36
+ yes?: Desc;
37
+ no?: Desc;
38
+ };
39
+ /** Any question the decision surface accepts. */
40
+ export type Question = ChoiceQuestion<string> | ScoreQuestion | NoulQuestion;
41
+ /** Selected label, calibration confidence, and a probability per label. */
42
+ export type ChoiceAnswer<L extends string> = {
43
+ kind: 'choice';
44
+ label: L;
45
+ confidence: number;
46
+ probabilities: Record<L, number>;
47
+ };
48
+ /** Rubric score, confidence, per-level legend, and a probability per level. */
49
+ export type ScoreAnswer = {
50
+ kind: 'score';
51
+ score: number;
52
+ confidence: number;
53
+ legend: Record<number, Desc>;
54
+ probabilities: Record<number, number>;
55
+ };
56
+ /**
57
+ * Bare yes-probability — no `confidence`, on purpose. The wire response
58
+ * returns only a probability; synthesizing a confidence would fabricate
59
+ * calibration data. The asymmetry is load-bearing: later code cannot paper
60
+ * over it.
61
+ */
62
+ export type NoulAnswer = {
63
+ kind: 'noul';
64
+ probability: number;
65
+ };
66
+ /** Any answer the decision surface returns — discriminate on `kind`. */
67
+ export type Answer = ChoiceAnswer<string> | ScoreAnswer | NoulAnswer;
68
+ /** The answer type a question of shape `Q` decodes to. */
69
+ export type AnswerFor<Q> = Q extends ChoiceQuestion<infer L> ? ChoiceAnswer<L> : Q extends ScoreQuestion ? ScoreAnswer : Q extends NoulQuestion ? NoulAnswer : never;
70
+ /** Conditional answer record for a keyed question map. */
71
+ export type AnswersFor<Q extends Record<string, Question>> = {
72
+ readonly [K in keyof Q]: AnswerFor<Q[K]>;
73
+ };
74
+ /**
75
+ * Question builders for the batch path. Namespaced under `q` because
76
+ * `choice` / `score` / `noul` are already `DecisionMaker` method names.
77
+ */
78
+ export declare const q: {
79
+ /**
80
+ * Build a choice question. `const` on the type parameter keeps an inline
81
+ * label map from widening to `string` — the caller keeps the union.
82
+ */
83
+ choice: <const L extends string>(prompt: Desc, labels: Record<L, Desc>) => ChoiceQuestion<L>;
84
+ /** Build a score question from a rubric of at least two levels. */
85
+ score: (prompt: Desc, rubric: readonly [Desc, Desc, ...Desc[]]) => ScoreQuestion;
86
+ /** Build a yes/no question; outcomes are optional and default undescribed. */
87
+ noul: (prompt?: Desc, outcomes?: {
88
+ yes?: Desc;
89
+ no?: Desc;
90
+ }) => NoulQuestion;
91
+ };
92
+ /**
93
+ * The one-method internal seam every backend driver implements. The loose
94
+ * `Record<string, Answer>` return is narrowed to `AnswersFor<Q>` by a single
95
+ * documented cast at the facade boundary — drivers stay trivial to write.
96
+ */
97
+ export interface DecisionDriver {
98
+ readonly name: string;
99
+ ask(req: {
100
+ state: DecisionState;
101
+ questions: Record<string, Question>;
102
+ model?: string;
103
+ }): Promise<Record<string, Answer>>;
104
+ }
105
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/decision/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Provider-neutral decision vocabulary — deliberately not re-exported from
3
+ * `@typesafe-ai/sdk`. Re-exporting the SDK's response types would bind every
4
+ * caller to the vendor; the neutral names here (`kind`, `labels`, `rubric`,
5
+ * `probability`) keep vendor mapping explicit and confined to the driver.
6
+ * This file imports nothing — no SDK, no workspace package (boundary rule).
7
+ */
8
+ /**
9
+ * Question builders for the batch path. Namespaced under `q` because
10
+ * `choice` / `score` / `noul` are already `DecisionMaker` method names.
11
+ */
12
+ export const q = {
13
+ /**
14
+ * Build a choice question. `const` on the type parameter keeps an inline
15
+ * label map from widening to `string` — the caller keeps the union.
16
+ */
17
+ choice: (prompt, labels) => ({
18
+ kind: 'choice',
19
+ prompt,
20
+ labels,
21
+ }),
22
+ /** Build a score question from a rubric of at least two levels. */
23
+ score: (prompt, rubric) => ({
24
+ kind: 'score',
25
+ prompt,
26
+ rubric,
27
+ }),
28
+ /** Build a yes/no question; outcomes are optional and default undescribed. */
29
+ noul: (prompt, outcomes) => ({
30
+ kind: 'noul',
31
+ prompt,
32
+ ...outcomes,
33
+ }),
34
+ };
@@ -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 './types';
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/decision/typesafe-driver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwBH,OAAO,KAAK,EAAU,cAAc,EAAY,MAAM,SAAS,CAAC;AAGhE,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,CA8CjF"}