@owlmeans/llm-common 0.1.18-rc.38 → 0.1.18-rc.39

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
@@ -20,7 +20,7 @@ extends these; `@owlmeans/llm` implements against them.
20
20
  ## Installation
21
21
 
22
22
  ```bash
23
- bun add @owlmeans/llm-common@^0.1.18-rc.38
23
+ bun add @owlmeans/llm-common@^0.1.18-rc.39
24
24
  ```
25
25
 
26
26
  ## Usage
@@ -96,7 +96,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
96
96
  your project's skill store (`.agents/skills/`):
97
97
 
98
98
  ```sh
99
- npx @owlmeans/agent-skills@^0.1.18-rc.48
99
+ npx @owlmeans/agent-skills@^0.1.18-rc.49
100
100
  ```
101
101
 
102
102
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/llm-common",
4
- "version": "0.1.18-rc.38",
5
- "generatedAt": "2026-10-05T08:32:19.123Z",
4
+ "version": "0.1.18-rc.39",
5
+ "generatedAt": "2026-10-05T21:03:06.400Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/llm-common
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/llm-common": "^0.1.18-rc.38"` in `dependencies`
11
+ **Install:** `"@owlmeans/llm-common": "^0.1.18-rc.39"` in `dependencies`
12
12
 
13
13
  The contracts half of the LLM stack. **No `@langchain/*` runtime dependency** — importable
14
14
  from a browser bundle, a queue worker, or any package that must not pull an inference SDK.
@@ -0,0 +1,47 @@
1
+ import type { Inquiry, InquiryAnswer } from '../types.js';
2
+ /**
3
+ * Pure helpers over an inquiry and its answer — no IO, no state, no clock.
4
+ *
5
+ * They are here rather than in the runtime because every layer needs the same reading of an
6
+ * answer: the execution service, the pipeline runner, the `ask_user` tool, the connector and the
7
+ * screen that finally shows it. A second reading of "was this answered" is a second contract.
8
+ */
9
+ export interface InquiryHelper {
10
+ /**
11
+ * What to assume when nobody answers: the question's own default, or a decline.
12
+ *
13
+ * A decline is an ANSWER — the run carries on and records what it assumed — which is why this
14
+ * never throws and never returns null.
15
+ */
16
+ defaultAnswerFor: (inquiry: Inquiry) => InquiryAnswer;
17
+ /**
18
+ * The one thing an answer says, as a string — the first chosen value, else the free text, else
19
+ * `null` when it says nothing at all (a decline, or an empty answer).
20
+ */
21
+ answeredWith: (answer: InquiryAnswer) => string | null;
22
+ /** Nobody could decide. Distinct from an empty answer — see {@link InquiryHelper.answeredWith}. */
23
+ isDeclined: (answer: InquiryAnswer) => boolean;
24
+ /**
25
+ * Cut an answer's free TEXT to the ceiling and SAY SO.
26
+ *
27
+ * Only the prose is cut. A `value` IS the decision — for a choice it has to equal one of the
28
+ * question's own option values — so slicing one does not degrade an answer, it silently replaces
29
+ * it with an identifier nobody offered and nothing matches, which `answeredWith` then hands on as
30
+ * what the person chose. An over-long value is a defect upstream rather than a long answer (the
31
+ * connector's schema refuses one outright instead of shortening it), so it travels on whole and is
32
+ * REPORTED through `truncated` — the flag every consumer already reads as "this is not exactly
33
+ * what the person gave".
34
+ */
35
+ capAnswer: (answer: InquiryAnswer, max?: number) => InquiryAnswer;
36
+ /**
37
+ * The copy of an answer a resumable pipeline STATE keeps: the decision whole, the prose cut to
38
+ * {@link INQUIRY_STATE_TEXT_CHARS}.
39
+ *
40
+ * The full answer goes back to whoever asked; only this reduced one is persisted, so a run that
41
+ * asks several questions still holds a state made of keys rather than of paragraphs.
42
+ */
43
+ stateAnswerOf: (answer: InquiryAnswer) => InquiryAnswer;
44
+ /** One-line label of a question — for a note, a trace line or a run row. */
45
+ renderInquiry: (inquiry: Inquiry) => string;
46
+ }
47
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/inquiry/helper/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEzD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,aAAa,CAAA;IACrD;;;OAGG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,CAAA;IACtD,mGAAmG;IACnG,UAAU,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAA;IAC9C;;;;;;;;;;OAUG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,aAAa,CAAA;IACjE;;;;;;OAMG;IACH,aAAa,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,aAAa,CAAA;IACvD,4EAA4E;IAC5E,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAA;CAC5C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/inquiry/helper/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import type { InquiryHelper } from './helper/types.js';
2
+ export declare const createInquiryHelper: () => InquiryHelper;
3
+ export declare const inquiryHelper: InquiryHelper;
4
+ //# sourceMappingURL=helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../../src/inquiry/helper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,eAAO,MAAM,mBAAmB,QAAO,aA8CtC,CAAA;AAED,eAAO,MAAM,aAAa,eAAwB,CAAA"}
@@ -0,0 +1,39 @@
1
+ import { DEFAULT_INQUIRY_ANSWER_CHARS, INQUIRY_STATE_TEXT_CHARS } from './consts.js';
2
+ export const createInquiryHelper = () => {
3
+ const defaultAnswerFor = (inquiry) => inquiry.default != null
4
+ ? { inquiryId: inquiry.id, value: inquiry.default }
5
+ : { inquiryId: inquiry.id, declined: true };
6
+ const answeredWith = (answer) => {
7
+ const value = Array.isArray(answer.value) ? answer.value[0] : answer.value;
8
+ if (value != null && value !== '')
9
+ return value;
10
+ if (answer.text != null && answer.text !== '')
11
+ return answer.text;
12
+ return null;
13
+ };
14
+ const isDeclined = (answer) => answer.declined === true;
15
+ const capAnswer = (answer, max = DEFAULT_INQUIRY_ANSWER_CHARS) => {
16
+ const text = answer.text != null && answer.text.length > max
17
+ ? answer.text.slice(0, max)
18
+ : undefined;
19
+ const values = Array.isArray(answer.value)
20
+ ? answer.value
21
+ : answer.value != null ? [answer.value] : [];
22
+ const oversized = values.some(value => value.length > max);
23
+ if (text == null && !oversized)
24
+ return answer;
25
+ return { ...answer, ...(text != null ? { text } : {}), truncated: true };
26
+ };
27
+ const stateAnswerOf = (answer) => answer.text != null && answer.text.length > INQUIRY_STATE_TEXT_CHARS
28
+ ? { ...answer, text: answer.text.slice(0, INQUIRY_STATE_TEXT_CHARS), truncated: true }
29
+ : answer;
30
+ const renderInquiry = (inquiry) => {
31
+ const options = inquiry.options != null && inquiry.options.length > 0
32
+ ? ` (${inquiry.options.map(option => option.value).join(' | ')})`
33
+ : '';
34
+ return `[${inquiry.kind}] ${inquiry.question}${options}`.replace(/\s+/g, ' ').trim();
35
+ };
36
+ return { defaultAnswerFor, answeredWith, isDeclined, capAnswer, stateAnswerOf, renderInquiry };
37
+ };
38
+ export const inquiryHelper = createInquiryHelper();
39
+ //# sourceMappingURL=helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.js","sourceRoot":"","sources":["../../src/inquiry/helper.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  export * from './consts.js';
2
2
  export type * from './types.js';
3
+ export type * from './helper/types.js';
4
+ export * from './helper.js';
3
5
  export * from './utils.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/inquiry/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,mBAAmB,YAAY,CAAA;AAC/B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/inquiry/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,mBAAmB,YAAY,CAAA;AAC/B,mBAAmB,mBAAmB,CAAA;AACtC,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA"}
@@ -1,3 +1,4 @@
1
1
  export * from './consts.js';
2
+ export * from './helper.js';
2
3
  export * from './utils.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/inquiry/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAE3B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/inquiry/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAG3B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA"}
@@ -1,45 +1,14 @@
1
1
  import type { Inquiry, InquiryAnswer } from './types.js';
2
- /**
3
- * Pure helpers over an inquiry and its answer — no IO, no state, no clock.
4
- *
5
- * They are here rather than in the runtime because every layer needs the same reading of an
6
- * answer: the execution service, the pipeline runner, the `ask_user` tool, the connector and the
7
- * screen that finally shows it. A second reading of "was this answered" is a second contract.
8
- */
9
- /**
10
- * What to assume when nobody answers: the question's own default, or a decline.
11
- *
12
- * A decline is an ANSWER — the run carries on and records what it assumed — which is why this
13
- * never throws and never returns null.
14
- */
2
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.defaultAnswerFor(…)` */
15
3
  export declare const defaultAnswerFor: (inquiry: Inquiry) => InquiryAnswer;
16
- /**
17
- * The one thing an answer says, as a string — the first chosen value, else the free text, else
18
- * `null` when it says nothing at all (a decline, or an empty answer).
19
- */
4
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.answeredWith(…)` */
20
5
  export declare const answeredWith: (answer: InquiryAnswer) => string | null;
21
- /** Nobody could decide. Distinct from an empty answer — see {@link answeredWith}. */
6
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.isDeclined(…)` */
22
7
  export declare const isDeclined: (answer: InquiryAnswer) => boolean;
23
- /**
24
- * Cut an answer's free TEXT to the ceiling and SAY SO.
25
- *
26
- * Only the prose is cut. A `value` IS the decision — for a choice it has to equal one of the
27
- * question's own option values — so slicing one does not degrade an answer, it silently replaces
28
- * it with an identifier nobody offered and nothing matches, which `answeredWith` then hands on as
29
- * what the person chose. An over-long value is a defect upstream rather than a long answer (the
30
- * connector's schema refuses one outright instead of shortening it), so it travels on whole and is
31
- * REPORTED through `truncated` — the flag every consumer already reads as "this is not exactly
32
- * what the person gave".
33
- */
8
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.capAnswer(…)` */
34
9
  export declare const capAnswer: (answer: InquiryAnswer, max?: number) => InquiryAnswer;
35
- /**
36
- * The copy of an answer a resumable pipeline STATE keeps: the decision whole, the prose cut to
37
- * {@link INQUIRY_STATE_TEXT_CHARS}.
38
- *
39
- * The full answer goes back to whoever asked; only this reduced one is persisted, so a run that
40
- * asks several questions still holds a state made of keys rather than of paragraphs.
41
- */
10
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.stateAnswerOf(…)` */
42
11
  export declare const stateAnswerOf: (answer: InquiryAnswer) => InquiryAnswer;
43
- /** One-line label of a question — for a note, a trace line or a run row. */
12
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.renderInquiry(…)` */
44
13
  export declare const renderInquiry: (inquiry: Inquiry) => string;
45
14
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/inquiry/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAExD;;;;;;GAMG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,YAAa,OAAO,KAAG,aAGL,CAAA;AAE/C;;;GAGG;AACH,eAAO,MAAM,YAAY,WAAY,aAAa,KAAG,MAAM,GAAG,IAM7D,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,UAAU,WAAY,aAAa,KAAG,OAAmC,CAAA;AAEtF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,WACZ,aAAa,QAAO,MAAM,KACjC,aAYF,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,WAAY,aAAa,KAAG,aAG1C,CAAA;AAEZ,4EAA4E;AAC5E,eAAO,MAAM,aAAa,YAAa,OAAO,KAAG,MAMhD,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/inquiry/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAExD,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,YAAa,OAAO,KAAG,aAAwD,CAAA;AAE5G,gFAAgF;AAChF,eAAO,MAAM,YAAY,WAAY,aAAa,KAAG,MAAM,GAAG,IAA0C,CAAA;AAExG,8EAA8E;AAC9E,eAAO,MAAM,UAAU,WAAY,aAAa,KAAG,OAA2C,CAAA;AAE9F,6EAA6E;AAC7E,eAAO,MAAM,SAAS,WAAY,aAAa,QAAQ,MAAM,KAAG,aAAqD,CAAA;AAErH,iFAAiF;AACjF,eAAO,MAAM,aAAa,WAAY,aAAa,KAAG,aAAoD,CAAA;AAE1G,iFAAiF;AACjF,eAAO,MAAM,aAAa,YAAa,OAAO,KAAG,MAA8C,CAAA"}
@@ -1,72 +1,14 @@
1
- import { DEFAULT_INQUIRY_ANSWER_CHARS, INQUIRY_STATE_TEXT_CHARS } from './consts.js';
2
- /**
3
- * Pure helpers over an inquiry and its answer — no IO, no state, no clock.
4
- *
5
- * They are here rather than in the runtime because every layer needs the same reading of an
6
- * answer: the execution service, the pipeline runner, the `ask_user` tool, the connector and the
7
- * screen that finally shows it. A second reading of "was this answered" is a second contract.
8
- */
9
- /**
10
- * What to assume when nobody answers: the question's own default, or a decline.
11
- *
12
- * A decline is an ANSWER — the run carries on and records what it assumed — which is why this
13
- * never throws and never returns null.
14
- */
15
- export const defaultAnswerFor = (inquiry) => inquiry.default != null
16
- ? { inquiryId: inquiry.id, value: inquiry.default }
17
- : { inquiryId: inquiry.id, declined: true };
18
- /**
19
- * The one thing an answer says, as a string — the first chosen value, else the free text, else
20
- * `null` when it says nothing at all (a decline, or an empty answer).
21
- */
22
- export const answeredWith = (answer) => {
23
- const value = Array.isArray(answer.value) ? answer.value[0] : answer.value;
24
- if (value != null && value !== '')
25
- return value;
26
- if (answer.text != null && answer.text !== '')
27
- return answer.text;
28
- return null;
29
- };
30
- /** Nobody could decide. Distinct from an empty answer — see {@link answeredWith}. */
31
- export const isDeclined = (answer) => answer.declined === true;
32
- /**
33
- * Cut an answer's free TEXT to the ceiling and SAY SO.
34
- *
35
- * Only the prose is cut. A `value` IS the decision — for a choice it has to equal one of the
36
- * question's own option values — so slicing one does not degrade an answer, it silently replaces
37
- * it with an identifier nobody offered and nothing matches, which `answeredWith` then hands on as
38
- * what the person chose. An over-long value is a defect upstream rather than a long answer (the
39
- * connector's schema refuses one outright instead of shortening it), so it travels on whole and is
40
- * REPORTED through `truncated` — the flag every consumer already reads as "this is not exactly
41
- * what the person gave".
42
- */
43
- export const capAnswer = (answer, max = DEFAULT_INQUIRY_ANSWER_CHARS) => {
44
- const text = answer.text != null && answer.text.length > max
45
- ? answer.text.slice(0, max)
46
- : undefined;
47
- const values = Array.isArray(answer.value)
48
- ? answer.value
49
- : answer.value != null ? [answer.value] : [];
50
- const oversized = values.some(value => value.length > max);
51
- if (text == null && !oversized)
52
- return answer;
53
- return { ...answer, ...(text != null ? { text } : {}), truncated: true };
54
- };
55
- /**
56
- * The copy of an answer a resumable pipeline STATE keeps: the decision whole, the prose cut to
57
- * {@link INQUIRY_STATE_TEXT_CHARS}.
58
- *
59
- * The full answer goes back to whoever asked; only this reduced one is persisted, so a run that
60
- * asks several questions still holds a state made of keys rather than of paragraphs.
61
- */
62
- export const stateAnswerOf = (answer) => answer.text != null && answer.text.length > INQUIRY_STATE_TEXT_CHARS
63
- ? { ...answer, text: answer.text.slice(0, INQUIRY_STATE_TEXT_CHARS), truncated: true }
64
- : answer;
65
- /** One-line label of a question — for a note, a trace line or a run row. */
66
- export const renderInquiry = (inquiry) => {
67
- const options = inquiry.options != null && inquiry.options.length > 0
68
- ? ` (${inquiry.options.map(option => option.value).join(' | ')})`
69
- : '';
70
- return `[${inquiry.kind}] ${inquiry.question}${options}`.replace(/\s+/g, ' ').trim();
71
- };
1
+ import { inquiryHelper } from './helper.js';
2
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.defaultAnswerFor(…)` */
3
+ export const defaultAnswerFor = (inquiry) => inquiryHelper.defaultAnswerFor(inquiry);
4
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.answeredWith(…)` */
5
+ export const answeredWith = (answer) => inquiryHelper.answeredWith(answer);
6
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.isDeclined(…)` */
7
+ export const isDeclined = (answer) => inquiryHelper.isDeclined(answer);
8
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.capAnswer(…)` */
9
+ export const capAnswer = (answer, max) => inquiryHelper.capAnswer(answer, max);
10
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.stateAnswerOf(…)` */
11
+ export const stateAnswerOf = (answer) => inquiryHelper.stateAnswerOf(answer);
12
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.renderInquiry(…)` */
13
+ export const renderInquiry = (inquiry) => inquiryHelper.renderInquiry(inquiry);
72
14
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/inquiry/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/inquiry/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAG3C,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAgB,EAAiB,EAAE,CAAC,aAAa,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;AAE5G,gFAAgF;AAChF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAqB,EAAiB,EAAE,CAAC,aAAa,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;AAExG,8EAA8E;AAC9E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAqB,EAAW,EAAE,CAAC,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AAE9F,6EAA6E;AAC7E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAqB,EAAE,GAAY,EAAiB,EAAE,CAAC,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAErH,iFAAiF;AACjF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAqB,EAAiB,EAAE,CAAC,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;AAE1G,iFAAiF;AACjF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,OAAgB,EAAU,EAAE,CAAC,aAAa,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/llm-common",
3
- "version": "0.1.18-rc.38",
3
+ "version": "0.1.18-rc.39",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,47 @@
1
+ import type { Inquiry, InquiryAnswer } from '../types.js'
2
+
3
+ /**
4
+ * Pure helpers over an inquiry and its answer — no IO, no state, no clock.
5
+ *
6
+ * They are here rather than in the runtime because every layer needs the same reading of an
7
+ * answer: the execution service, the pipeline runner, the `ask_user` tool, the connector and the
8
+ * screen that finally shows it. A second reading of "was this answered" is a second contract.
9
+ */
10
+ export interface InquiryHelper {
11
+ /**
12
+ * What to assume when nobody answers: the question's own default, or a decline.
13
+ *
14
+ * A decline is an ANSWER — the run carries on and records what it assumed — which is why this
15
+ * never throws and never returns null.
16
+ */
17
+ defaultAnswerFor: (inquiry: Inquiry) => InquiryAnswer
18
+ /**
19
+ * The one thing an answer says, as a string — the first chosen value, else the free text, else
20
+ * `null` when it says nothing at all (a decline, or an empty answer).
21
+ */
22
+ answeredWith: (answer: InquiryAnswer) => string | null
23
+ /** Nobody could decide. Distinct from an empty answer — see {@link InquiryHelper.answeredWith}. */
24
+ isDeclined: (answer: InquiryAnswer) => boolean
25
+ /**
26
+ * Cut an answer's free TEXT to the ceiling and SAY SO.
27
+ *
28
+ * Only the prose is cut. A `value` IS the decision — for a choice it has to equal one of the
29
+ * question's own option values — so slicing one does not degrade an answer, it silently replaces
30
+ * it with an identifier nobody offered and nothing matches, which `answeredWith` then hands on as
31
+ * what the person chose. An over-long value is a defect upstream rather than a long answer (the
32
+ * connector's schema refuses one outright instead of shortening it), so it travels on whole and is
33
+ * REPORTED through `truncated` — the flag every consumer already reads as "this is not exactly
34
+ * what the person gave".
35
+ */
36
+ capAnswer: (answer: InquiryAnswer, max?: number) => InquiryAnswer
37
+ /**
38
+ * The copy of an answer a resumable pipeline STATE keeps: the decision whole, the prose cut to
39
+ * {@link INQUIRY_STATE_TEXT_CHARS}.
40
+ *
41
+ * The full answer goes back to whoever asked; only this reduced one is persisted, so a run that
42
+ * asks several questions still holds a state made of keys rather than of paragraphs.
43
+ */
44
+ stateAnswerOf: (answer: InquiryAnswer) => InquiryAnswer
45
+ /** One-line label of a question — for a note, a trace line or a run row. */
46
+ renderInquiry: (inquiry: Inquiry) => string
47
+ }
@@ -0,0 +1,53 @@
1
+ import { DEFAULT_INQUIRY_ANSWER_CHARS, INQUIRY_STATE_TEXT_CHARS } from './consts.js'
2
+ import type { Inquiry, InquiryAnswer } from './types.js'
3
+ import type { InquiryHelper } from './helper/types.js'
4
+
5
+ export const createInquiryHelper = (): InquiryHelper => {
6
+ const defaultAnswerFor = (inquiry: Inquiry): InquiryAnswer =>
7
+ inquiry.default != null
8
+ ? { inquiryId: inquiry.id, value: inquiry.default }
9
+ : { inquiryId: inquiry.id, declined: true }
10
+
11
+ const answeredWith = (answer: InquiryAnswer): string | null => {
12
+ const value = Array.isArray(answer.value) ? answer.value[0] : answer.value
13
+ if (value != null && value !== '') return value
14
+ if (answer.text != null && answer.text !== '') return answer.text
15
+
16
+ return null
17
+ }
18
+
19
+ const isDeclined = (answer: InquiryAnswer): boolean => answer.declined === true
20
+
21
+ const capAnswer = (
22
+ answer: InquiryAnswer, max: number = DEFAULT_INQUIRY_ANSWER_CHARS
23
+ ): InquiryAnswer => {
24
+ const text = answer.text != null && answer.text.length > max
25
+ ? answer.text.slice(0, max)
26
+ : undefined
27
+ const values = Array.isArray(answer.value)
28
+ ? answer.value
29
+ : answer.value != null ? [answer.value] : []
30
+ const oversized = values.some(value => value.length > max)
31
+
32
+ if (text == null && !oversized) return answer
33
+
34
+ return { ...answer, ...(text != null ? { text } : {}), truncated: true }
35
+ }
36
+
37
+ const stateAnswerOf = (answer: InquiryAnswer): InquiryAnswer =>
38
+ answer.text != null && answer.text.length > INQUIRY_STATE_TEXT_CHARS
39
+ ? { ...answer, text: answer.text.slice(0, INQUIRY_STATE_TEXT_CHARS), truncated: true }
40
+ : answer
41
+
42
+ const renderInquiry = (inquiry: Inquiry): string => {
43
+ const options = inquiry.options != null && inquiry.options.length > 0
44
+ ? ` (${inquiry.options.map(option => option.value).join(' | ')})`
45
+ : ''
46
+
47
+ return `[${inquiry.kind}] ${inquiry.question}${options}`.replace(/\s+/g, ' ').trim()
48
+ }
49
+
50
+ return { defaultAnswerFor, answeredWith, isDeclined, capAnswer, stateAnswerOf, renderInquiry }
51
+ }
52
+
53
+ export const inquiryHelper = createInquiryHelper()
@@ -1,3 +1,5 @@
1
1
  export * from './consts.js'
2
2
  export type * from './types.js'
3
+ export type * from './helper/types.js'
4
+ export * from './helper.js'
3
5
  export * from './utils.js'
@@ -1,84 +1,20 @@
1
- import { DEFAULT_INQUIRY_ANSWER_CHARS, INQUIRY_STATE_TEXT_CHARS } from './consts.js'
1
+ import { inquiryHelper } from './helper.js'
2
2
  import type { Inquiry, InquiryAnswer } from './types.js'
3
3
 
4
- /**
5
- * Pure helpers over an inquiry and its answer — no IO, no state, no clock.
6
- *
7
- * They are here rather than in the runtime because every layer needs the same reading of an
8
- * answer: the execution service, the pipeline runner, the `ask_user` tool, the connector and the
9
- * screen that finally shows it. A second reading of "was this answered" is a second contract.
10
- */
4
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.defaultAnswerFor(…)` */
5
+ export const defaultAnswerFor = (inquiry: Inquiry): InquiryAnswer => inquiryHelper.defaultAnswerFor(inquiry)
11
6
 
12
- /**
13
- * What to assume when nobody answers: the question's own default, or a decline.
14
- *
15
- * A decline is an ANSWER — the run carries on and records what it assumed — which is why this
16
- * never throws and never returns null.
17
- */
18
- export const defaultAnswerFor = (inquiry: Inquiry): InquiryAnswer =>
19
- inquiry.default != null
20
- ? { inquiryId: inquiry.id, value: inquiry.default }
21
- : { inquiryId: inquiry.id, declined: true }
7
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.answeredWith(…)` */
8
+ export const answeredWith = (answer: InquiryAnswer): string | null => inquiryHelper.answeredWith(answer)
22
9
 
23
- /**
24
- * The one thing an answer says, as a string — the first chosen value, else the free text, else
25
- * `null` when it says nothing at all (a decline, or an empty answer).
26
- */
27
- export const answeredWith = (answer: InquiryAnswer): string | null => {
28
- const value = Array.isArray(answer.value) ? answer.value[0] : answer.value
29
- if (value != null && value !== '') return value
30
- if (answer.text != null && answer.text !== '') return answer.text
10
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.isDeclined(…)` */
11
+ export const isDeclined = (answer: InquiryAnswer): boolean => inquiryHelper.isDeclined(answer)
31
12
 
32
- return null
33
- }
13
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.capAnswer(…)` */
14
+ export const capAnswer = (answer: InquiryAnswer, max?: number): InquiryAnswer => inquiryHelper.capAnswer(answer, max)
34
15
 
35
- /** Nobody could decide. Distinct from an empty answer — see {@link answeredWith}. */
36
- export const isDeclined = (answer: InquiryAnswer): boolean => answer.declined === true
16
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.stateAnswerOf(…)` */
17
+ export const stateAnswerOf = (answer: InquiryAnswer): InquiryAnswer => inquiryHelper.stateAnswerOf(answer)
37
18
 
38
- /**
39
- * Cut an answer's free TEXT to the ceiling and SAY SO.
40
- *
41
- * Only the prose is cut. A `value` IS the decision — for a choice it has to equal one of the
42
- * question's own option values — so slicing one does not degrade an answer, it silently replaces
43
- * it with an identifier nobody offered and nothing matches, which `answeredWith` then hands on as
44
- * what the person chose. An over-long value is a defect upstream rather than a long answer (the
45
- * connector's schema refuses one outright instead of shortening it), so it travels on whole and is
46
- * REPORTED through `truncated` — the flag every consumer already reads as "this is not exactly
47
- * what the person gave".
48
- */
49
- export const capAnswer = (
50
- answer: InquiryAnswer, max: number = DEFAULT_INQUIRY_ANSWER_CHARS
51
- ): InquiryAnswer => {
52
- const text = answer.text != null && answer.text.length > max
53
- ? answer.text.slice(0, max)
54
- : undefined
55
- const values = Array.isArray(answer.value)
56
- ? answer.value
57
- : answer.value != null ? [answer.value] : []
58
- const oversized = values.some(value => value.length > max)
59
-
60
- if (text == null && !oversized) return answer
61
-
62
- return { ...answer, ...(text != null ? { text } : {}), truncated: true }
63
- }
64
-
65
- /**
66
- * The copy of an answer a resumable pipeline STATE keeps: the decision whole, the prose cut to
67
- * {@link INQUIRY_STATE_TEXT_CHARS}.
68
- *
69
- * The full answer goes back to whoever asked; only this reduced one is persisted, so a run that
70
- * asks several questions still holds a state made of keys rather than of paragraphs.
71
- */
72
- export const stateAnswerOf = (answer: InquiryAnswer): InquiryAnswer =>
73
- answer.text != null && answer.text.length > INQUIRY_STATE_TEXT_CHARS
74
- ? { ...answer, text: answer.text.slice(0, INQUIRY_STATE_TEXT_CHARS), truncated: true }
75
- : answer
76
-
77
- /** One-line label of a question — for a note, a trace line or a run row. */
78
- export const renderInquiry = (inquiry: Inquiry): string => {
79
- const options = inquiry.options != null && inquiry.options.length > 0
80
- ? ` (${inquiry.options.map(option => option.value).join(' | ')})`
81
- : ''
82
-
83
- return `[${inquiry.kind}] ${inquiry.question}${options}`.replace(/\s+/g, ' ').trim()
84
- }
19
+ /** @deprecated compat:factory-refactor — use `inquiryHelper.renderInquiry(…)` */
20
+ export const renderInquiry = (inquiry: Inquiry): string => inquiryHelper.renderInquiry(inquiry)
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
- answeredWith, capAnswer, CONFIRM_NO, CONFIRM_YES, defaultAnswerFor, DEFAULT_INQUIRY_ANSWER_CHARS,
4
- INQUIRY_STATE_TEXT_CHARS, InquiryKind, isDeclined, renderInquiry, stateAnswerOf,
3
+ CONFIRM_NO, CONFIRM_YES, DEFAULT_INQUIRY_ANSWER_CHARS, INQUIRY_STATE_TEXT_CHARS, InquiryKind, inquiryHelper,
5
4
  } from '../src/index.js'
6
5
  import type { Inquiry, InquiryAnswer } from '../src/index.js'
7
6
 
@@ -24,61 +23,61 @@ const inquiry = (patch: Partial<Inquiry> = {}): Inquiry => ({
24
23
 
25
24
  describe('@owlmeans/llm-common — what an unanswered question assumes', () => {
26
25
  test('a question with a default answers itself with it', () => {
27
- expect(defaultAnswerFor(inquiry({ default: 'postgres' })))
26
+ expect(inquiryHelper.defaultAnswerFor(inquiry({ default: 'postgres' })))
28
27
  .toEqual({ inquiryId: 'q1', value: 'postgres' })
29
28
  })
30
29
 
31
30
  test('a question without one declines — a decision, never a failure', () => {
32
- const answer = defaultAnswerFor(inquiry())
33
- expect(isDeclined(answer)).toBe(true)
34
- expect(answeredWith(answer)).toBeNull()
31
+ const answer = inquiryHelper.defaultAnswerFor(inquiry())
32
+ expect(inquiryHelper.isDeclined(answer)).toBe(true)
33
+ expect(inquiryHelper.answeredWith(answer)).toBeNull()
35
34
  })
36
35
 
37
36
  test('a multiple-choice default travels whole', () => {
38
- expect(defaultAnswerFor(inquiry({ multiple: true, default: ['postgres', 'mongo'] })).value)
37
+ expect(inquiryHelper.defaultAnswerFor(inquiry({ multiple: true, default: ['postgres', 'mongo'] })).value)
39
38
  .toEqual(['postgres', 'mongo'])
40
39
  })
41
40
  })
42
41
 
43
42
  describe('@owlmeans/llm-common — reading one answer', () => {
44
43
  test('the chosen value wins over free text, and the first of a list is the answer', () => {
45
- expect(answeredWith({ inquiryId: 'q1', value: 'mongo', text: 'or postgres' })).toBe('mongo')
46
- expect(answeredWith({ inquiryId: 'q1', value: ['mongo', 'redis'] })).toBe('mongo')
44
+ expect(inquiryHelper.answeredWith({ inquiryId: 'q1', value: 'mongo', text: 'or postgres' })).toBe('mongo')
45
+ expect(inquiryHelper.answeredWith({ inquiryId: 'q1', value: ['mongo', 'redis'] })).toBe('mongo')
47
46
  })
48
47
 
49
48
  test('free text answers a question that offered no options', () => {
50
- expect(answeredWith({ inquiryId: 'q1', text: 'the vendor one' })).toBe('the vendor one')
49
+ expect(inquiryHelper.answeredWith({ inquiryId: 'q1', text: 'the vendor one' })).toBe('the vendor one')
51
50
  })
52
51
 
53
52
  test('an answer that says nothing reads as nothing, declined or not', () => {
54
- expect(answeredWith({ inquiryId: 'q1' })).toBeNull()
55
- expect(answeredWith({ inquiryId: 'q1', value: '', text: '' })).toBeNull()
56
- expect(isDeclined({ inquiryId: 'q1' })).toBe(false)
57
- expect(isDeclined({ inquiryId: 'q1', declined: true })).toBe(true)
53
+ expect(inquiryHelper.answeredWith({ inquiryId: 'q1' })).toBeNull()
54
+ expect(inquiryHelper.answeredWith({ inquiryId: 'q1', value: '', text: '' })).toBeNull()
55
+ expect(inquiryHelper.isDeclined({ inquiryId: 'q1' })).toBe(false)
56
+ expect(inquiryHelper.isDeclined({ inquiryId: 'q1', declined: true })).toBe(true)
58
57
  })
59
58
 
60
59
  test('a confirmation is just its two values', () => {
61
- expect(answeredWith({ inquiryId: 'q1', value: CONFIRM_YES })).toBe(CONFIRM_YES)
62
- expect(answeredWith({ inquiryId: 'q1', value: CONFIRM_NO })).toBe(CONFIRM_NO)
60
+ expect(inquiryHelper.answeredWith({ inquiryId: 'q1', value: CONFIRM_YES })).toBe(CONFIRM_YES)
61
+ expect(inquiryHelper.answeredWith({ inquiryId: 'q1', value: CONFIRM_NO })).toBe(CONFIRM_NO)
63
62
  })
64
63
  })
65
64
 
66
65
  describe('@owlmeans/llm-common — the one ceiling', () => {
67
66
  test('an answer inside the ceiling is left exactly as it came', () => {
68
67
  const answer: InquiryAnswer = { inquiryId: 'q1', value: 'mongo', text: 'because of the driver' }
69
- expect(capAnswer(answer)).toEqual(answer)
70
- expect(capAnswer(answer).truncated).toBeUndefined()
68
+ expect(inquiryHelper.capAnswer(answer)).toEqual(answer)
69
+ expect(inquiryHelper.capAnswer(answer).truncated).toBeUndefined()
71
70
  })
72
71
 
73
72
  test('a cut is never silent', () => {
74
- const capped = capAnswer({ inquiryId: 'q1', text: 'x'.repeat(DEFAULT_INQUIRY_ANSWER_CHARS + 5) })
73
+ const capped = inquiryHelper.capAnswer({ inquiryId: 'q1', text: 'x'.repeat(DEFAULT_INQUIRY_ANSWER_CHARS + 5) })
75
74
  expect(capped.text).toHaveLength(DEFAULT_INQUIRY_ANSWER_CHARS)
76
75
  expect(capped.truncated).toBe(true)
77
76
  })
78
77
 
79
78
  test('an over-long value is reported, never shortened into an option nobody offered', () => {
80
79
  const value = ['ok', 'y'.repeat(40)]
81
- const capped = capAnswer({ inquiryId: 'q1', value }, 10)
80
+ const capped = inquiryHelper.capAnswer({ inquiryId: 'q1', value }, 10)
82
81
  // Cutting prose degrades an answer; cutting an identifier CHANGES it — `answeredWith` would
83
82
  // then hand the caller a choice the question never carried.
84
83
  expect(capped.value).toEqual(value)
@@ -89,24 +88,24 @@ describe('@owlmeans/llm-common — the one ceiling', () => {
89
88
  const answer: InquiryAnswer = {
90
89
  inquiryId: 'q1', value: 'postgres', text: 'z'.repeat(INQUIRY_STATE_TEXT_CHARS + 100),
91
90
  }
92
- const stored = stateAnswerOf(answer)
91
+ const stored = inquiryHelper.stateAnswerOf(answer)
93
92
  expect(stored.value).toBe('postgres')
94
93
  expect(stored.text).toHaveLength(INQUIRY_STATE_TEXT_CHARS)
95
94
  expect(stored.truncated).toBe(true)
96
95
  // The answer handed back to whoever asked is untouched — only the persisted copy is reduced.
97
96
  expect(answer.text).toHaveLength(INQUIRY_STATE_TEXT_CHARS + 100)
98
- expect(stateAnswerOf({ inquiryId: 'q1', value: 'postgres' }).truncated).toBeUndefined()
97
+ expect(inquiryHelper.stateAnswerOf({ inquiryId: 'q1', value: 'postgres' }).truncated).toBeUndefined()
99
98
  })
100
99
  })
101
100
 
102
101
  describe('@owlmeans/llm-common — one line for a trace', () => {
103
102
  test('the label names the kind, the question and the choices, on one line', () => {
104
- expect(renderInquiry(inquiry({ question: 'Which\n database?' })))
103
+ expect(inquiryHelper.renderInquiry(inquiry({ question: 'Which\n database?' })))
105
104
  .toBe('[choice] Which database? (postgres | mongo)')
106
105
  })
107
106
 
108
107
  test('a question with no options renders without an empty bracket', () => {
109
- expect(renderInquiry({ id: 'q2', kind: InquiryKind.Confirm, question: 'Relocate the origin?' }))
108
+ expect(inquiryHelper.renderInquiry({ id: 'q2', kind: InquiryKind.Confirm, question: 'Relocate the origin?' }))
110
109
  .toBe('[confirm] Relocate the origin?')
111
110
  })
112
111
  })