@itsoltech/basal-client 0.1.0 → 0.3.0

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
@@ -57,8 +57,8 @@ in `cause`. `BasalHTTPError.body` remains directly accessible but is non-enumera
57
57
  inspection and JSON.stringify do not log it. There are no automatic retries or redirects. Types describe the native contract; successful
58
58
  responses are checked to be JSON objects without full schema validation. Unknown JSON fields are preserved.
59
59
 
60
- Supports `choice`, `noul`, `score`, `multi`, `act`, `facts` and `evidence`. Native choice/score accepts 2–10
61
- options. Criterion insertion order matters; JavaScript enumerates integer-like object keys numerically,
60
+ Supports `choice`, `noul`, `score`, `multi`, `act` and `tool`, with model-dependent `facts` and `evidence` capabilities. Native choice/score accepts 2–255
61
+ options for format-2 models (older model limits are checked by the server). Criterion insertion order matters; JavaScript enumerates integer-like object keys numerically,
62
62
  so use textual keys (e.g. `low`, `high`) or an ordered score array. Evidence offsets count Unicode code
63
63
  points; use the returned text, or `Array.from(state).slice(start, end).join("")` for textual states.
64
64
 
@@ -76,3 +76,55 @@ retains the native JSON without those defaults. The one-argument raw `decide(req
76
76
 
77
77
 
78
78
  [Full documentation and publication setup](https://github.com/itsoltech/basal-rs/blob/main/docs/SDK.md).
79
+
80
+
81
+ ## Basal 2.0 and Omni
82
+
83
+ Typed choice/score and act outcomes allow 2–255 options. The server remains
84
+ responsible for limits of older models and for calibration/model capabilities.
85
+ `tool` accepts OpenAI, MCP and Anthropic definition objects, optional non-tool
86
+ choices (`answer_directly`, `ask_user`, `none`), ranking, multi selection and
87
+ certified deferral. The SDK returns the decision; it never executes tools.
88
+
89
+ Multi selection supports `threshold`, `exact`, `iou` and `f1`, count limits,
90
+ `exclusive`, `fallback` and `fallback_threshold`. `threshold` is optional in
91
+ results because objective-based responses instead contain `objective` and
92
+ `expected_score`; constraint diagnostics remain available. Single tool selection
93
+ can be null on certified deferral, while multi mode returns a tool list.
94
+
95
+ Typed request options include `lang` (`pl`/`en`), ordered inline `media` and
96
+ `detail` (`default`/`high`). Media helpers accept encoded bytes, base64 or a data
97
+ URL; they never fetch URLs. Read files explicitly in your application. Decoding,
98
+ model support and resource limits are checked by the server. Omni requires a
99
+ CUDA server started with media enabled. `calibration: "text"` means text
100
+ calibration applied to media, not validated calibration for images/audio.
101
+
102
+ Usage exposes `longest_prompt_tokens`, `second_order_branches` and media counts,
103
+ soft tokens and detail. Discovery exposes `prompt_format`, `orders`, enabled
104
+ media and calibration temperatures. Model names remain strings: use exact names
105
+ from model discovery, not a hardcoded model enumeration. Keep old models and
106
+ raw JSON access available. The server validates each model’s capabilities.
107
+ Basal 2.1 task helpers are described below.
108
+
109
+ ```typescript
110
+ import { BasalClient, image, tool, multi } from "@itsoltech/basal-client";
111
+ const response = await new BasalClient().decide("Inspect the supplied material.", {
112
+ route: tool("Choose a tool.", [{name:"inspect", description:"Inspect material", inputSchema:{type:"object"}}], {extra:["none"], rank:2, description_chars:120}),
113
+ labels: multi("Select visible objects.", {animal:"An animal", vehicle:"A vehicle"}, {objective:"f1"}),
114
+ }, {model:"basal-omni-1.0-mini", lang:"en", media:[image(new Uint8Array(await file.arrayBuffer()))]});
115
+ console.log(response.answers.route, response.answers.labels.expected_score);
116
+ ```
117
+
118
+ Tool descriptions are normalized by the server. Omit `description_chars`
119
+ (`DescriptionChars` in .NET) to keep the default cap of 300 Unicode code points;
120
+ use `0` to disable truncation. Descriptions longer than a positive cap are
121
+ shortened at a word boundary where possible, with an ellipsis appended. The SDK forwards this option
122
+ without changing the supplied tool definitions.
123
+
124
+ ## Experimental Basal 2.1 tasks
125
+
126
+ Typed classify, verify, compress and extract factories preserve nullable task automation,
127
+ Unicode code point offsets and nested JSON extraction values. Task answers do not share
128
+ the primitive probabilities field. See [task semantics](../../docs/TASKS.md) and
129
+ [SDK examples](../../docs/SDK.md#zadania-basal-21). These source changes require a server
130
+ with the experimental task layer; installing an SDK does not update the inference server.
@@ -17,6 +17,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.BasalClient = exports.BasalResponseTooLargeError = exports.BasalProtocolError = exports.BasalTransportError = exports.BasalHTTPError = exports.BasalError = void 0;
18
18
  const questions_cjs_1 = require("./questions.cjs");
19
19
  __exportStar(require("./questions.cjs"), exports);
20
+ __exportStar(require("./media.cjs"), exports);
21
+ __exportStar(require("./tasks.cjs"), exports);
20
22
  class BasalError extends Error {
21
23
  }
22
24
  exports.BasalError = BasalError;
@@ -1,11 +1,31 @@
1
1
  import { type QuestionSet, type TypedDecisionResponse, type DecisionOptions } from "./questions.cjs";
2
2
  export * from "./questions.cjs";
3
+ export * from "./media.cjs";
4
+ export * from "./tasks.cjs";
5
+ import type { TaskAnswer } from "./tasks.cjs";
3
6
  /** Native Basal wire types. Criterion order is significant; do not sort keys. */
4
7
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
5
8
  [key: string]: JsonValue;
6
9
  };
10
+ export type Calibration = "validated" | "unvalidated" | "text";
11
+ export type MultiObjective = "threshold" | "exact" | "iou" | "f1";
12
+ export interface MediaInput {
13
+ type: "image" | "audio";
14
+ data: string;
15
+ }
16
+ export interface MediaUsage {
17
+ images: number;
18
+ audio: number;
19
+ media_tokens: number;
20
+ detail: "default" | "high";
21
+ }
22
+ export interface ToolRanking {
23
+ tool: string;
24
+ probability: number;
25
+ }
26
+ export type ToolDefinition = Record<string, JsonValue>;
7
27
  export interface Question {
8
- type?: "choice" | "noul" | "score" | "multi" | "act";
28
+ type?: "choice" | "noul" | "score" | "multi" | "act" | "tool" | "classify" | "verify" | "compress" | "extract";
9
29
  instructions?: JsonValue;
10
30
  criteria?: Record<string, JsonValue> | JsonValue[];
11
31
  levels?: Record<string, JsonValue> | JsonValue[];
@@ -17,13 +37,34 @@ export interface Question {
17
37
  base?: "noul" | "choice" | "score";
18
38
  costs?: Record<string, number> | Record<string, Record<string, number>>;
19
39
  defer_action?: string;
20
- max_error?: number;
40
+ max_error?: number | readonly number[];
41
+ objective?: MultiObjective;
42
+ exclusive?: readonly string[];
43
+ fallback?: string;
44
+ fallback_threshold?: number;
45
+ tools?: readonly ToolDefinition[];
46
+ extra?: readonly ("answer_directly" | "ask_user" | "none")[];
47
+ rank?: number;
48
+ description_chars?: number;
49
+ multi?: boolean;
50
+ labels?: Record<string, JsonValue> | JsonValue[];
51
+ multi_label?: boolean;
52
+ none?: boolean;
53
+ claims?: Record<string, JsonValue> | JsonValue[];
54
+ binary?: boolean;
55
+ unit?: 'sentence' | 'line' | 'paragraph';
56
+ keep_ratio?: number;
57
+ min_keep?: number;
58
+ schema?: Record<string, JsonValue>;
21
59
  }
22
60
  export interface DecisionRequest {
23
61
  state: JsonValue;
24
62
  questions: Record<string, Question>;
25
63
  model?: string;
26
64
  facts?: "off" | "auto";
65
+ lang?: "pl" | "en";
66
+ media?: readonly MediaInput[];
67
+ detail?: "default" | "high";
27
68
  }
28
69
  export interface EvidenceSpan {
29
70
  text: string;
@@ -34,6 +75,7 @@ export interface EvidenceSpan {
34
75
  interface BaseAnswer<K extends string = string> {
35
76
  probabilities: Record<K, number>;
36
77
  evidence?: EvidenceSpan[];
78
+ calibration?: Calibration;
37
79
  }
38
80
  export interface ChoiceAnswer<K extends string = string> extends BaseAnswer<K> {
39
81
  type: "choice";
@@ -51,29 +93,57 @@ export interface ScoreAnswer extends BaseAnswer {
51
93
  legend: Record<string, string>;
52
94
  confidence: number;
53
95
  }
54
- export interface MultiAnswer<K extends string = string> extends BaseAnswer<K> {
96
+ export interface SelectionResult<K extends string = string> {
97
+ threshold?: number;
98
+ objective?: MultiObjective;
99
+ expected_score?: number | null;
100
+ constraints_applied?: JsonValue;
101
+ set_confidence: number;
102
+ }
103
+ export interface MultiAnswer<K extends string = string> extends BaseAnswer<K>, SelectionResult<K> {
55
104
  type: "multi";
56
105
  selected: K[];
57
- threshold: number;
58
- set_confidence: number;
59
106
  }
107
+ export interface SingleToolAnswer extends BaseAnswer {
108
+ type: "tool";
109
+ multi?: false;
110
+ tool: string | null;
111
+ call: boolean;
112
+ confidence: number;
113
+ ranking?: ToolRanking[];
114
+ deferred?: boolean;
115
+ refused?: string;
116
+ max_error?: string;
117
+ }
118
+ export interface MultiToolAnswer extends BaseAnswer, SelectionResult {
119
+ type: "tool";
120
+ multi: true;
121
+ tools: string[];
122
+ call: boolean;
123
+ ranking?: ToolRanking[];
124
+ }
125
+ export type ToolAnswer = SingleToolAnswer | MultiToolAnswer;
60
126
  export interface ActAnswer<K extends string = string, A extends string = string> extends BaseAnswer<K> {
61
127
  type: "act";
62
128
  action: A;
63
129
  answer: K;
64
130
  expected_costs: Record<A, number>;
65
131
  confidence: number;
66
- calibration: "validated" | "unvalidated";
132
+ calibration: Calibration;
67
133
  refused?: string;
68
134
  max_error?: string;
135
+ deferred?: boolean;
69
136
  }
70
- export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer | MultiAnswer | ActAnswer;
137
+ export type Answer = ChoiceAnswer | NoulAnswer | ScoreAnswer | MultiAnswer | ActAnswer | ToolAnswer | TaskAnswer;
71
138
  export interface Usage {
72
139
  input_tokens: number;
73
140
  output_tokens: number;
74
141
  questions?: number;
75
142
  branches?: number;
76
143
  latency_ms?: number;
144
+ longest_prompt_tokens?: number;
145
+ second_order_branches?: number;
146
+ media?: MediaUsage;
77
147
  }
78
148
  export interface DecisionResponse {
79
149
  model: string;
@@ -86,6 +156,10 @@ export interface ModelMetadata {
86
156
  release_date?: string;
87
157
  mode?: string;
88
158
  early_exit?: JsonValue[];
159
+ prompt_format?: number;
160
+ orders?: JsonValue;
161
+ temperature_per_prim?: Record<string, number>;
162
+ media?: ("image" | "audio")[];
89
163
  }
90
164
  export interface ModelsResponse {
91
165
  models: ModelMetadata[];
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.image = image;
4
+ exports.audio = audio;
5
+ function media(type, data) {
6
+ if (data instanceof Uint8Array) {
7
+ // Bounded chunks avoid the argument limit of spreading a whole image/audio file.
8
+ const chunks = [];
9
+ for (let offset = 0; offset < data.length; offset += 8192)
10
+ chunks.push(String.fromCharCode(...data.subarray(offset, offset + 8192)));
11
+ data = btoa(chunks.join(''));
12
+ }
13
+ if (typeof data !== 'string' || !data.trim() || /^https?:\/\//i.test(data))
14
+ throw new TypeError('media data must be bytes, base64 or a data URL');
15
+ return { type, data };
16
+ }
17
+ /** Inline image; never fetches a URL or reads a file. */
18
+ function image(data) { return media('image', data); }
19
+ /** Inline audio; never fetches a URL or reads a file. */
20
+ function audio(data) { return media('audio', data); }
@@ -0,0 +1,5 @@
1
+ import type { MediaInput } from './index.cjs';
2
+ /** Inline image; never fetches a URL or reads a file. */
3
+ export declare function image(data: Uint8Array | string): MediaInput;
4
+ /** Inline audio; never fetches a URL or reads a file. */
5
+ export declare function audio(data: Uint8Array | string): MediaInput;
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ActQuestion = exports.MultiQuestion = exports.ScoreQuestion = exports.NoulQuestion = exports.ChoiceQuestion = exports.QuestionSpec = void 0;
3
+ exports.ActQuestion = exports.ToolQuestion = exports.MultiQuestion = exports.ScoreQuestion = exports.NoulQuestion = exports.ChoiceQuestion = exports.QuestionSpec = void 0;
4
4
  exports.choice = choice;
5
5
  exports.noul = noul;
6
6
  exports.score = score;
7
7
  exports.multi = multi;
8
8
  exports.act = act;
9
+ exports.tool = tool;
9
10
  exports.buildRequest = buildRequest;
10
11
  function text(value, name) {
11
12
  if (typeof value !== 'string' || !value.trim())
@@ -39,19 +40,20 @@ function evidence(options) {
39
40
  class QuestionSpec {
40
41
  wire;
41
42
  constructor(kind, instructions, fields) {
42
- this.wire = structuredClone({ ...fields, type: kind, instructions: text(instructions, 'instructions') });
43
+ this.wire = structuredClone({ ...fields, type: kind, ...(['classify', 'verify', 'compress', 'extract'].includes(kind) ? (instructions ? { instructions } : {}) : { instructions: text(instructions, 'instructions') }) });
43
44
  }
44
45
  toJSON() { return structuredClone(this.wire); }
45
46
  /** Validate and retrieve an answer with this question's result type. */
46
47
  read(response, id) {
47
48
  const answer = response.answers?.[id];
48
- if (!answer || answer.type !== this.wire.type || !answer.probabilities || typeof answer.probabilities !== 'object' || Array.isArray(answer.probabilities)) {
49
+ if (!answer || !('probabilities' in answer) || answer.type !== this.wire.type || !answer.probabilities || typeof answer.probabilities !== 'object' || Array.isArray(answer.probabilities)) {
49
50
  throw new TypeError('Basal returned a missing or mismatched answer');
50
51
  }
51
52
  const fields = {
52
53
  choice: { choice: 'string', confidence: 'number' }, noul: { noul: 'number', confidence: 'number' },
53
54
  score: { score: 'number', legend: 'object', confidence: 'number' },
54
- multi: { selected: 'object', threshold: 'number', set_confidence: 'number' },
55
+ multi: { selected: 'object', set_confidence: 'number' },
56
+ tool: answer.type === 'tool' && answer.multi ? { tools: 'object', call: 'boolean', set_confidence: 'number' } : { call: 'boolean', confidence: 'number' },
55
57
  act: { action: 'string', answer: 'string', expected_costs: 'object', confidence: 'number', calibration: 'string' },
56
58
  };
57
59
  const data = answer;
@@ -84,14 +86,45 @@ class QuestionSpec {
84
86
  if (answer.type === 'score' && (Array.isArray(answer.legend) || Object.values(answer.legend).some(v => typeof v !== 'string')))
85
87
  throw new TypeError('invalid legend');
86
88
  if (answer.type === 'act') {
87
- if (!['validated', 'unvalidated'].includes(answer.calibration) || !Object.hasOwn(this.wire.costs ?? {}, answer.action) || !expectedKeys.includes(answer.answer))
89
+ if (!['validated', 'unvalidated', 'text'].includes(answer.calibration) || !Object.hasOwn(this.wire.costs ?? {}, answer.action) || !expectedKeys.includes(answer.answer))
88
90
  throw new TypeError('invalid action');
89
91
  if (Array.isArray(answer.expected_costs) || Object.keys(this.wire.costs ?? {}).some(key => !Object.hasOwn(answer.expected_costs, key)))
90
92
  throw new TypeError('invalid expected costs');
91
93
  for (const value of Object.values(answer.expected_costs))
92
94
  number(value, 'expected cost');
93
95
  }
94
- return { ...answer, ...(answer.type === 'multi' ? {} : { evidence: answer.evidence ?? [] }) };
96
+ if (answer.type === 'multi' || (answer.type === 'tool' && answer.multi)) {
97
+ validateSelection(answer, this.wire.objective ?? 'threshold');
98
+ }
99
+ if (answer.type === 'tool') {
100
+ if ((answer.multi ?? false) !== (this.wire.multi ?? false))
101
+ throw new TypeError('mismatched tool mode');
102
+ if (answer.multi) {
103
+ if (!Array.isArray(answer.tools) || answer.tools.some(k => typeof k !== 'string' || !Object.hasOwn(answer.probabilities, k)))
104
+ throw new TypeError('invalid selected tools');
105
+ }
106
+ else {
107
+ if (answer.tool === null ? answer.deferred !== true : typeof answer.tool !== 'string' || !Object.hasOwn(answer.probabilities, answer.tool))
108
+ throw new TypeError('invalid selected tool');
109
+ if (answer.deferred !== undefined && typeof answer.deferred !== 'boolean')
110
+ throw new TypeError('invalid deferred');
111
+ for (const key of ['refused', 'max_error'])
112
+ if (answer[key] !== undefined && typeof answer[key] !== 'string')
113
+ throw new TypeError('invalid tool metadata');
114
+ }
115
+ if (answer.ranking !== undefined) {
116
+ if (!Array.isArray(answer.ranking))
117
+ throw new TypeError('invalid ranking');
118
+ for (const entry of answer.ranking) {
119
+ if (!entry || typeof entry.tool !== 'string' || !Object.hasOwn(answer.probabilities, entry.tool))
120
+ throw new TypeError('invalid ranking entry');
121
+ number(entry.probability, 'ranking probability', true);
122
+ }
123
+ }
124
+ }
125
+ if (answer.type === 'act' && answer.deferred !== undefined && typeof answer.deferred !== 'boolean')
126
+ throw new TypeError('invalid deferred');
127
+ return { ...answer, ...(answer.type === 'multi' || answer.type === 'tool' ? {} : { evidence: answer.evidence ?? [] }) };
95
128
  }
96
129
  }
97
130
  exports.QuestionSpec = QuestionSpec;
@@ -99,7 +132,7 @@ class ChoiceQuestion extends QuestionSpec {
99
132
  constructor(instructions, values, options = {}) {
100
133
  if (options.option_keys !== undefined && !['show', 'hide'].includes(options.option_keys))
101
134
  throw new TypeError('invalid option_keys');
102
- super('choice', instructions, { criteria: criteria(values, 2, 10), ...evidence(options), ...(options.option_keys === 'hide' ? { option_keys: 'hide' } : {}) });
135
+ super('choice', instructions, { criteria: criteria(values, 2, 255), ...evidence(options), ...(options.option_keys === 'hide' ? { option_keys: 'hide' } : {}) });
103
136
  }
104
137
  }
105
138
  exports.ChoiceQuestion = ChoiceQuestion;
@@ -109,36 +142,106 @@ class NoulQuestion extends QuestionSpec {
109
142
  exports.NoulQuestion = NoulQuestion;
110
143
  class ScoreQuestion extends QuestionSpec {
111
144
  constructor(instructions, levels, options = {}) {
112
- if (!Array.isArray(levels) || levels.length < 2 || levels.length > 10)
113
- throw new TypeError('score requires 2–10 ordered levels');
145
+ if (!Array.isArray(levels) || levels.length < 2 || levels.length > 255)
146
+ throw new TypeError('score requires 2–255 ordered levels');
114
147
  super('score', instructions, { criteria: levels.map(v => text(v, 'level')), ...evidence(options) });
115
148
  }
116
149
  }
117
150
  exports.ScoreQuestion = ScoreQuestion;
151
+ function selectionOptions(options, keys) {
152
+ const fields = {};
153
+ const objective = options.objective ?? 'threshold';
154
+ if (!['threshold', 'exact', 'iou', 'f1'].includes(objective))
155
+ throw new TypeError('invalid objective');
156
+ if (objective !== 'threshold')
157
+ fields.objective = objective;
158
+ if (options.threshold !== undefined) {
159
+ if (objective !== 'threshold')
160
+ throw new TypeError('threshold applies only to objective threshold');
161
+ fields.threshold = number(options.threshold, 'threshold', true);
162
+ }
163
+ for (const key of ['min', 'max']) {
164
+ const v = options[key];
165
+ if (v !== undefined) {
166
+ if (!Number.isSafeInteger(v) || v < 0 || (keys && v > keys.length))
167
+ throw new TypeError(`invalid ${key}`);
168
+ fields[key] = v;
169
+ }
170
+ }
171
+ if (options.min !== undefined && options.max !== undefined && options.min > options.max)
172
+ throw new TypeError('min exceeds max');
173
+ if (options.exclusive !== undefined) {
174
+ if (!Array.isArray(options.exclusive))
175
+ throw new TypeError('exclusive must be an array');
176
+ fields.exclusive = options.exclusive.map(k => { text(k, 'exclusive key'); if (keys && !keys.includes(k))
177
+ throw new TypeError('unknown exclusive key'); return k; });
178
+ }
179
+ if (options.fallback !== undefined) {
180
+ fields.fallback = text(options.fallback, 'fallback');
181
+ if (keys && !keys.includes(options.fallback))
182
+ throw new TypeError('unknown fallback');
183
+ }
184
+ if (options.fallback_threshold !== undefined) {
185
+ if (options.fallback === undefined)
186
+ throw new TypeError('fallback_threshold requires fallback');
187
+ fields.fallback_threshold = number(options.fallback_threshold, 'fallback_threshold', true);
188
+ }
189
+ return fields;
190
+ }
191
+ function validateSelection(answer, objective) {
192
+ number(answer.set_confidence, 'set_confidence', true);
193
+ if (objective === 'threshold')
194
+ number(answer.threshold, 'threshold', true);
195
+ else {
196
+ if (answer.objective !== objective)
197
+ throw new TypeError('mismatched objective');
198
+ if (answer.expected_score !== null && answer.expected_score !== undefined)
199
+ number(answer.expected_score, 'expected_score', true);
200
+ }
201
+ }
118
202
  class MultiQuestion extends QuestionSpec {
119
203
  constructor(instructions, values, options = {}) {
120
204
  if ('evidence' in options)
121
205
  throw new TypeError('multi does not support evidence');
122
- const fields = { criteria: criteria(values, 1) };
123
- if (options.threshold !== undefined)
124
- fields.threshold = number(options.threshold, 'threshold', true);
125
- for (const key of ['min', 'max']) {
126
- const value = options[key];
127
- if (value !== undefined) {
128
- if (!Number.isInteger(value) || value < 0 || value > Object.keys(values).length)
129
- throw new TypeError(`invalid ${key}`);
130
- fields[key] = value;
131
- }
132
- }
133
- if (options.min !== undefined && options.max !== undefined && options.min > options.max)
134
- throw new TypeError('min exceeds max');
135
- super('multi', instructions, fields);
206
+ super('multi', instructions, { criteria: criteria(values, 1), ...selectionOptions(options, Object.keys(values)) });
136
207
  }
137
208
  }
138
209
  exports.MultiQuestion = MultiQuestion;
210
+ class ToolQuestion extends QuestionSpec {
211
+ constructor(instructions, tools, options = {}) {
212
+ if (!Array.isArray(tools) || tools.some(t => !t || typeof t !== 'object' || Array.isArray(t)))
213
+ throw new TypeError('tools must contain definition objects');
214
+ if (options.extra !== undefined && (!Array.isArray(options.extra) || options.extra.some(k => !['answer_directly', 'ask_user', 'none'].includes(k))))
215
+ throw new TypeError('invalid extra tool option');
216
+ if (options.multi !== undefined && typeof options.multi !== 'boolean')
217
+ throw new TypeError('invalid multi');
218
+ const fields = { tools, ...(options.extra === undefined ? {} : { extra: options.extra }) };
219
+ if (options.description_chars !== undefined) {
220
+ if (!Number.isSafeInteger(options.description_chars) || options.description_chars < 0)
221
+ throw new TypeError('description_chars must be a non-negative safe integer');
222
+ fields.description_chars = options.description_chars;
223
+ }
224
+ if (options.rank !== undefined) {
225
+ if (!Number.isSafeInteger(options.rank) || options.rank < 1)
226
+ throw new TypeError('rank must be a positive integer');
227
+ fields.rank = options.rank;
228
+ }
229
+ if (options.max_error !== undefined)
230
+ fields.max_error = number(options.max_error, 'max_error', true);
231
+ const selection = selectionOptions(options);
232
+ if (!options.multi && Object.keys(selection).length)
233
+ throw new TypeError('selection options require multi');
234
+ if (options.multi) {
235
+ fields.multi = true;
236
+ Object.assign(fields, selection);
237
+ }
238
+ super('tool', instructions, fields);
239
+ }
240
+ }
241
+ exports.ToolQuestion = ToolQuestion;
139
242
  class ActQuestion extends QuestionSpec {
140
243
  constructor(instructions, costs, options = {}) {
141
- const optionsMap = options.criteria === undefined ? undefined : criteria(options.criteria, 2, 10);
244
+ const optionsMap = options.criteria === undefined ? undefined : criteria(options.criteria, 2, 255);
142
245
  const outcomes = Object.keys(optionsMap ?? { 'true': 'Yes', 'false': 'No' });
143
246
  if (!costs || typeof costs !== 'object' || Array.isArray(costs) || !Object.keys(costs).length)
144
247
  throw new TypeError('act requires a cost matrix');
@@ -162,6 +265,7 @@ function noul(instructions, options) { return new NoulQuestion(instructions, opt
162
265
  function score(instructions, levels, options) { return new ScoreQuestion(instructions, levels, options); }
163
266
  function multi(instructions, values, options) { return new MultiQuestion(instructions, values, options); }
164
267
  function act(instructions, costs, options) { return new ActQuestion(instructions, costs, options); }
268
+ function tool(instructions, tools, options) { return new ToolQuestion(instructions, tools, options); }
165
269
  function buildRequest(state, questions, options) {
166
270
  if (state === null || state === undefined)
167
271
  throw new TypeError('state must not be null/undefined');
@@ -175,5 +279,11 @@ function buildRequest(state, questions, options) {
175
279
  text(options.model, 'model');
176
280
  if (options.facts !== undefined && !['off', 'auto'].includes(options.facts))
177
281
  throw new TypeError('invalid facts');
178
- return { state, questions: Object.fromEntries(entries), ...(options.model === undefined ? {} : { model: options.model }), ...(options.facts === undefined ? {} : { facts: options.facts }) };
282
+ if (options.lang !== undefined && !['pl', 'en'].includes(options.lang))
283
+ throw new TypeError('invalid lang');
284
+ if (options.detail !== undefined && !['default', 'high'].includes(options.detail))
285
+ throw new TypeError('invalid detail');
286
+ if (options.media !== undefined && (!Array.isArray(options.media) || options.media.length < 1 || options.media.length > 8 || options.media.some(m => !m || !['image', 'audio'].includes(m.type) || typeof m.data !== 'string' || !m.data.trim())))
287
+ throw new TypeError('invalid media');
288
+ return { state, questions: Object.fromEntries(entries), ...(options.lang === undefined ? {} : { lang: options.lang }), ...(options.detail === undefined ? {} : { detail: options.detail }), ...(options.media === undefined ? {} : { media: structuredClone(options.media) }), ...(options.model === undefined ? {} : { model: options.model }), ...(options.facts === undefined ? {} : { facts: options.facts }) };
179
289
  }
@@ -1,4 +1,4 @@
1
- import type { ActAnswer, Answer, ChoiceAnswer, DecisionResponse, EvidenceSpan, JsonValue, MultiAnswer, NoulAnswer, Question, ScoreAnswer } from './index.cjs';
1
+ import type { ActAnswer, Answer, ChoiceAnswer, DecisionResponse, EvidenceSpan, JsonValue, MultiAnswer, NoulAnswer, Question, ScoreAnswer, ToolAnswer, ToolDefinition, MultiObjective, DecisionRequest } from './index.cjs';
2
2
  /** Required result metadata on the typed path; missing spans become an empty array. */
3
3
  export type TypedChoiceAnswer<K extends string = string> = ChoiceAnswer<K> & {
4
4
  evidence: EvidenceSpan[];
@@ -19,19 +19,29 @@ export interface EvidenceOptions {
19
19
  export interface ChoiceOptions extends EvidenceOptions {
20
20
  option_keys?: 'show' | 'hide';
21
21
  }
22
- export interface MultiOptions {
22
+ export interface MultiOptions<K extends string = string> {
23
23
  threshold?: number;
24
24
  min?: number;
25
25
  max?: number;
26
+ objective?: MultiObjective;
27
+ exclusive?: readonly K[];
28
+ fallback?: K;
29
+ fallback_threshold?: number;
30
+ }
31
+ export interface ToolOptions extends MultiOptions {
32
+ extra?: readonly ("answer_directly" | "ask_user" | "none")[];
33
+ rank?: number;
34
+ multi?: boolean;
35
+ max_error?: number;
36
+ /** Tool-description truncation cap; omitted keeps the server default (300), zero disables truncation. */
37
+ description_chars?: number;
26
38
  }
27
39
  export interface ActOptions<K extends string = string, A extends string = string> extends EvidenceOptions {
28
40
  criteria?: Record<K, string>;
29
41
  defer_action?: A;
30
42
  max_error?: number;
31
43
  }
32
- export interface DecisionOptions {
33
- model?: string;
34
- facts?: 'off' | 'auto';
44
+ export interface DecisionOptions extends Pick<DecisionRequest, 'model' | 'facts' | 'lang' | 'media' | 'detail'> {
35
45
  signal?: AbortSignal;
36
46
  }
37
47
  /** Immutable snapshot of a typed question. Use factories or the concrete constructors. */
@@ -52,7 +62,10 @@ export declare class ScoreQuestion extends QuestionSpec<TypedScoreAnswer> {
52
62
  constructor(instructions: string, levels: readonly string[], options?: EvidenceOptions);
53
63
  }
54
64
  export declare class MultiQuestion<K extends string = string> extends QuestionSpec<TypedMultiAnswer<K>> {
55
- constructor(instructions: string, values: Record<K, string>, options?: MultiOptions);
65
+ constructor(instructions: string, values: Record<K, string>, options?: MultiOptions<K>);
66
+ }
67
+ export declare class ToolQuestion extends QuestionSpec<ToolAnswer> {
68
+ constructor(instructions: string, tools: readonly ToolDefinition[], options?: ToolOptions);
56
69
  }
57
70
  export declare class ActQuestion<K extends string = 'true' | 'false', A extends string = string> extends QuestionSpec<TypedActAnswer<K, A>> {
58
71
  constructor(instructions: string, costs: Record<A, Record<NoInfer<K>, number>>, options?: ActOptions<K, NoInfer<A>>);
@@ -60,8 +73,9 @@ export declare class ActQuestion<K extends string = 'true' | 'false', A extends
60
73
  export declare function choice<const K extends string>(instructions: string, values: Record<K, string>, options?: ChoiceOptions): ChoiceQuestion<K>;
61
74
  export declare function noul(instructions: string, options?: EvidenceOptions): NoulQuestion;
62
75
  export declare function score(instructions: string, levels: readonly string[], options?: EvidenceOptions): ScoreQuestion;
63
- export declare function multi<const K extends string>(instructions: string, values: Record<K, string>, options?: MultiOptions): MultiQuestion<K>;
76
+ export declare function multi<const K extends string>(instructions: string, values: Record<K, string>, options?: MultiOptions<K>): MultiQuestion<K>;
64
77
  export declare function act<const K extends string = 'true' | 'false', const A extends string = string>(instructions: string, costs: Record<A, Record<NoInfer<K>, number>>, options?: ActOptions<K, NoInfer<A>>): ActQuestion<K, A>;
78
+ export declare function tool(instructions: string, tools: readonly ToolDefinition[], options?: ToolOptions): ToolQuestion;
65
79
  export type QuestionSet = Record<string, QuestionSpec>;
66
80
  export type TypedDecisionResponse<Q extends QuestionSet> = Omit<DecisionResponse, 'answers'> & {
67
81
  answers: {
@@ -70,9 +84,4 @@ export type TypedDecisionResponse<Q extends QuestionSet> = Omit<DecisionResponse
70
84
  answer<K extends keyof Q & (string | number)>(key: K): Q[K] extends QuestionSpec<infer A> ? A : never;
71
85
  raw: DecisionResponse;
72
86
  };
73
- export declare function buildRequest(state: JsonValue, questions: QuestionSet, options: DecisionOptions): {
74
- state: JsonValue;
75
- questions: Record<string, Question>;
76
- model?: string;
77
- facts?: 'off' | 'auto';
78
- };
87
+ export declare function buildRequest(state: JsonValue, questions: QuestionSet, options: DecisionOptions): DecisionRequest;