@owlmeans/llm-common 0.1.18-rc.2 → 0.1.18-rc.21

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.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/llm-common/SKILL.md +29 -6
  4. package/build/consts.d.ts +22 -1
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +21 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/delegate/index.d.ts +2 -0
  9. package/build/delegate/index.d.ts.map +1 -0
  10. package/build/delegate/index.js +2 -0
  11. package/build/delegate/index.js.map +1 -0
  12. package/build/delegate/types.d.ts +108 -0
  13. package/build/delegate/types.d.ts.map +1 -0
  14. package/build/delegate/types.js +39 -0
  15. package/build/delegate/types.js.map +1 -0
  16. package/build/files/types.d.ts +10 -0
  17. package/build/files/types.d.ts.map +1 -1
  18. package/build/index.d.ts +2 -0
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js +2 -0
  21. package/build/index.js.map +1 -1
  22. package/build/inquiry/consts.d.ts +48 -0
  23. package/build/inquiry/consts.d.ts.map +1 -0
  24. package/build/inquiry/consts.js +50 -0
  25. package/build/inquiry/consts.js.map +1 -0
  26. package/build/inquiry/index.d.ts +4 -0
  27. package/build/inquiry/index.d.ts.map +1 -0
  28. package/build/inquiry/index.js +3 -0
  29. package/build/inquiry/index.js.map +1 -0
  30. package/build/inquiry/types.d.ts +72 -0
  31. package/build/inquiry/types.d.ts.map +1 -0
  32. package/build/inquiry/types.js +2 -0
  33. package/build/inquiry/types.js.map +1 -0
  34. package/build/inquiry/utils.d.ts +45 -0
  35. package/build/inquiry/utils.d.ts.map +1 -0
  36. package/build/inquiry/utils.js +72 -0
  37. package/build/inquiry/utils.js.map +1 -0
  38. package/build/types.d.ts +35 -2
  39. package/build/types.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/src/consts.ts +22 -0
  42. package/src/delegate/index.ts +1 -0
  43. package/src/delegate/types.ts +116 -0
  44. package/src/files/types.ts +11 -0
  45. package/src/index.ts +2 -0
  46. package/src/inquiry/consts.ts +52 -0
  47. package/src/inquiry/index.ts +3 -0
  48. package/src/inquiry/types.ts +77 -0
  49. package/src/inquiry/utils.ts +84 -0
  50. package/src/types.ts +35 -2
  51. package/tests/inquiry.spec.ts +112 -0
  52. package/tsconfig.json +3 -1
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ExecutionEffort, ExecutionLevel, PromptBlock } from './consts.js'
2
+ import type { InquiryConfig } from './inquiry/types.js'
2
3
 
3
4
  /**
4
5
  * Free-form observability metadata attached to every model call — forwarded to the
@@ -32,6 +33,12 @@ export interface ModelConfigPatch {
32
33
  maxTokensCap?: number
33
34
  topP?: number
34
35
  disableThinking?: boolean
36
+ /** Total window the model accepts (input + output). Informational / validation only. */
37
+ contextWindow?: number
38
+ /** What the PROVIDER can emit in one request. Hard ceiling for `maxTokens`/`maxTokensCap`. */
39
+ maxOutput?: number
40
+ /** The window is shared between input and output rather than input-only. */
41
+ combinedWindow?: boolean
35
42
  }
36
43
 
37
44
  /** A JSON-safe model override: a config alias, or a partial config patch. */
@@ -49,6 +56,12 @@ export interface ModelPolicy {
49
56
  roleOverrides?: Partial<Record<ModelRole, ModelRole>>
50
57
  /** "Pin a role to a specific model/config" — alias or partial config override. */
51
58
  modelOverrides?: Partial<Record<ModelRole, ModelConfigOverride>>
59
+ /**
60
+ * Role resolved by `ExecutionService.utility` for cheap side calls. Defaults to
61
+ * `UTILITY_ROLE`; name another alias when the deployment calls its cheap tier
62
+ * something else. `roleOverrides` still applies on top of whichever one is used.
63
+ */
64
+ utilityRole?: ModelRole
52
65
  }
53
66
 
54
67
  /**
@@ -129,11 +142,24 @@ export interface ExecutionState {
129
142
  policy: ModelPolicy
130
143
  /** Role + skills for this level; merged downward by `ExecutionService`. */
131
144
  prompt?: PromptPolicy
145
+ /**
146
+ * How this run may put a question to a person. Serializable, and deliberately NOT a
147
+ * collaborator: a run that is resumed days later must ask through the same channel, under the
148
+ * same policy, as the one that parked it.
149
+ */
150
+ inquiry?: InquiryConfig
132
151
  }
133
152
 
134
- /** Resumable state of a task-level execution. */
153
+ /**
154
+ * The task level's own fields.
155
+ *
156
+ * `phase`, `completed` and `cursor` are LABELS — for a trace line, a prompt, a log — and never a
157
+ * workflow position. Recoverable position lives on a pipeline run row (`@owlmeans/agent`), which is
158
+ * a single authority; an execution that also claimed to know where a run stood would be a second
159
+ * one, and the two would disagree the first time a step wrote only one of them.
160
+ */
135
161
  export interface TaskExecutionState extends ExecutionState {
136
- /** Abstract workflow position for checkpoint/resume. */
162
+ /** A label for the stage a task considers itself in. Never read back to decide anything. */
137
163
  phase?: string
138
164
  completed?: string[]
139
165
  cursor?: string
@@ -181,11 +207,18 @@ export interface NullCapture {
181
207
  tool_calls?: unknown
182
208
  } | null
183
209
  diagnostics: {
210
+ /** Whatever the provider called it — OpenAI's `finish_reason` or Anthropic's `stop_reason`. */
184
211
  finishReason?: string
185
212
  inputTokens?: number
186
213
  outputTokens?: number
187
214
  reasoningTokens?: number
188
215
  contentEmpty: boolean
216
+ /**
217
+ * Content arrived, but none of it was text — the shape of an answer that was all reasoning.
218
+ * Distinguishes "spent the budget thinking" from "returned nothing at all", which
219
+ * `contentEmpty` alone cannot.
220
+ */
221
+ thinkingOnly?: boolean
189
222
  hadToolCall: boolean
190
223
  }
191
224
  }
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ answeredWith, capAnswer, CONFIRM_NO, CONFIRM_YES, defaultAnswerFor, DEFAULT_INQUIRY_ANSWER_CHARS,
4
+ INQUIRY_STATE_TEXT_CHARS, InquiryKind, isDeclined, renderInquiry, stateAnswerOf,
5
+ } from '../src/index.js'
6
+ import type { Inquiry, InquiryAnswer } from '../src/index.js'
7
+
8
+ /**
9
+ * The pure half of the inquiry primitive. Every layer that carries an answer — the execution
10
+ * service, the pipeline runner, the `ask_user` tool, the connector — reads it through exactly
11
+ * these functions, so a second reading of "was this answered" is what these specs exist to stop.
12
+ */
13
+
14
+ const inquiry = (patch: Partial<Inquiry> = {}): Inquiry => ({
15
+ id: 'q1',
16
+ kind: InquiryKind.Choice,
17
+ question: 'Which database does the origin use?',
18
+ options: [
19
+ { value: 'postgres', label: 'PostgreSQL' },
20
+ { value: 'mongo', label: 'MongoDB' },
21
+ ],
22
+ ...patch,
23
+ })
24
+
25
+ describe('@owlmeans/llm-common — what an unanswered question assumes', () => {
26
+ test('a question with a default answers itself with it', () => {
27
+ expect(defaultAnswerFor(inquiry({ default: 'postgres' })))
28
+ .toEqual({ inquiryId: 'q1', value: 'postgres' })
29
+ })
30
+
31
+ 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()
35
+ })
36
+
37
+ test('a multiple-choice default travels whole', () => {
38
+ expect(defaultAnswerFor(inquiry({ multiple: true, default: ['postgres', 'mongo'] })).value)
39
+ .toEqual(['postgres', 'mongo'])
40
+ })
41
+ })
42
+
43
+ describe('@owlmeans/llm-common — reading one answer', () => {
44
+ 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')
47
+ })
48
+
49
+ test('free text answers a question that offered no options', () => {
50
+ expect(answeredWith({ inquiryId: 'q1', text: 'the vendor one' })).toBe('the vendor one')
51
+ })
52
+
53
+ 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)
58
+ })
59
+
60
+ 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)
63
+ })
64
+ })
65
+
66
+ describe('@owlmeans/llm-common — the one ceiling', () => {
67
+ test('an answer inside the ceiling is left exactly as it came', () => {
68
+ const answer: InquiryAnswer = { inquiryId: 'q1', value: 'mongo', text: 'because of the driver' }
69
+ expect(capAnswer(answer)).toEqual(answer)
70
+ expect(capAnswer(answer).truncated).toBeUndefined()
71
+ })
72
+
73
+ test('a cut is never silent', () => {
74
+ const capped = capAnswer({ inquiryId: 'q1', text: 'x'.repeat(DEFAULT_INQUIRY_ANSWER_CHARS + 5) })
75
+ expect(capped.text).toHaveLength(DEFAULT_INQUIRY_ANSWER_CHARS)
76
+ expect(capped.truncated).toBe(true)
77
+ })
78
+
79
+ test('an over-long value is reported, never shortened into an option nobody offered', () => {
80
+ const value = ['ok', 'y'.repeat(40)]
81
+ const capped = capAnswer({ inquiryId: 'q1', value }, 10)
82
+ // Cutting prose degrades an answer; cutting an identifier CHANGES it — `answeredWith` would
83
+ // then hand the caller a choice the question never carried.
84
+ expect(capped.value).toEqual(value)
85
+ expect(capped.truncated).toBe(true)
86
+ })
87
+
88
+ test('the state copy keeps the decision whole and cuts only the prose', () => {
89
+ const answer: InquiryAnswer = {
90
+ inquiryId: 'q1', value: 'postgres', text: 'z'.repeat(INQUIRY_STATE_TEXT_CHARS + 100),
91
+ }
92
+ const stored = stateAnswerOf(answer)
93
+ expect(stored.value).toBe('postgres')
94
+ expect(stored.text).toHaveLength(INQUIRY_STATE_TEXT_CHARS)
95
+ expect(stored.truncated).toBe(true)
96
+ // The answer handed back to whoever asked is untouched — only the persisted copy is reduced.
97
+ expect(answer.text).toHaveLength(INQUIRY_STATE_TEXT_CHARS + 100)
98
+ expect(stateAnswerOf({ inquiryId: 'q1', value: 'postgres' }).truncated).toBeUndefined()
99
+ })
100
+ })
101
+
102
+ describe('@owlmeans/llm-common — one line for a trace', () => {
103
+ test('the label names the kind, the question and the choices, on one line', () => {
104
+ expect(renderInquiry(inquiry({ question: 'Which\n database?' })))
105
+ .toBe('[choice] Which database? (postgres | mongo)')
106
+ })
107
+
108
+ test('a question with no options renders without an empty bracket', () => {
109
+ expect(renderInquiry({ id: 'q2', kind: InquiryKind.Confirm, question: 'Relocate the origin?' }))
110
+ .toBe('[confirm] Relocate the origin?')
111
+ })
112
+ })
package/tsconfig.json CHANGED
@@ -10,6 +10,8 @@
10
10
  ],
11
11
  "exclude": [
12
12
  "./dist/**/*",
13
- "./build/**/*"
13
+ "./build/**/*",
14
+ "./tests/**/*",
15
+ "./*.ts"
14
16
  ]
15
17
  }