@owlmeans/llm 0.1.14

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 (147) hide show
  1. package/README.md +193 -0
  2. package/agent-meta/instructions/llm.instructions.md +66 -0
  3. package/agent-meta/manifest.json +23 -0
  4. package/agent-meta/skills/llm/SKILL.md +121 -0
  5. package/build/consts.d.ts +67 -0
  6. package/build/consts.d.ts.map +1 -0
  7. package/build/consts.js +76 -0
  8. package/build/consts.js.map +1 -0
  9. package/build/errors.d.ts +33 -0
  10. package/build/errors.d.ts.map +1 -0
  11. package/build/errors.js +52 -0
  12. package/build/errors.js.map +1 -0
  13. package/build/execution/index.d.ts +4 -0
  14. package/build/execution/index.d.ts.map +1 -0
  15. package/build/execution/index.js +3 -0
  16. package/build/execution/index.js.map +1 -0
  17. package/build/execution/service.d.ts +21 -0
  18. package/build/execution/service.d.ts.map +1 -0
  19. package/build/execution/service.js +129 -0
  20. package/build/execution/service.js.map +1 -0
  21. package/build/execution/types.d.ts +119 -0
  22. package/build/execution/types.d.ts.map +1 -0
  23. package/build/execution/types.js +2 -0
  24. package/build/execution/types.js.map +1 -0
  25. package/build/execution/utils.d.ts +28 -0
  26. package/build/execution/utils.d.ts.map +1 -0
  27. package/build/execution/utils.js +60 -0
  28. package/build/execution/utils.js.map +1 -0
  29. package/build/helpers/index.d.ts +5 -0
  30. package/build/helpers/index.d.ts.map +1 -0
  31. package/build/helpers/index.js +5 -0
  32. package/build/helpers/index.js.map +1 -0
  33. package/build/helpers/json.d.ts +24 -0
  34. package/build/helpers/json.d.ts.map +1 -0
  35. package/build/helpers/json.js +119 -0
  36. package/build/helpers/json.js.map +1 -0
  37. package/build/helpers/messages.d.ts +10 -0
  38. package/build/helpers/messages.d.ts.map +1 -0
  39. package/build/helpers/messages.js +9 -0
  40. package/build/helpers/messages.js.map +1 -0
  41. package/build/helpers/retry.d.ts +18 -0
  42. package/build/helpers/retry.d.ts.map +1 -0
  43. package/build/helpers/retry.js +57 -0
  44. package/build/helpers/retry.js.map +1 -0
  45. package/build/helpers/spectate.d.ts +8 -0
  46. package/build/helpers/spectate.d.ts.map +1 -0
  47. package/build/helpers/spectate.js +54 -0
  48. package/build/helpers/spectate.js.map +1 -0
  49. package/build/index.d.ts +13 -0
  50. package/build/index.d.ts.map +1 -0
  51. package/build/index.js +11 -0
  52. package/build/index.js.map +1 -0
  53. package/build/model.d.ts +12 -0
  54. package/build/model.d.ts.map +1 -0
  55. package/build/model.js +297 -0
  56. package/build/model.js.map +1 -0
  57. package/build/plugins/anthropic.d.ts +4 -0
  58. package/build/plugins/anthropic.d.ts.map +1 -0
  59. package/build/plugins/anthropic.js +86 -0
  60. package/build/plugins/anthropic.js.map +1 -0
  61. package/build/plugins/compatible.d.ts +18 -0
  62. package/build/plugins/compatible.d.ts.map +1 -0
  63. package/build/plugins/compatible.js +54 -0
  64. package/build/plugins/compatible.js.map +1 -0
  65. package/build/plugins/export.d.ts +6 -0
  66. package/build/plugins/export.d.ts.map +1 -0
  67. package/build/plugins/export.js +5 -0
  68. package/build/plugins/export.js.map +1 -0
  69. package/build/plugins/index.d.ts +18 -0
  70. package/build/plugins/index.d.ts.map +1 -0
  71. package/build/plugins/index.js +42 -0
  72. package/build/plugins/index.js.map +1 -0
  73. package/build/plugins/openai.d.ts +28 -0
  74. package/build/plugins/openai.d.ts.map +1 -0
  75. package/build/plugins/openai.js +89 -0
  76. package/build/plugins/openai.js.map +1 -0
  77. package/build/plugins/types.d.ts +80 -0
  78. package/build/plugins/types.d.ts.map +1 -0
  79. package/build/plugins/types.js +2 -0
  80. package/build/plugins/types.js.map +1 -0
  81. package/build/plugins/utils.d.ts +27 -0
  82. package/build/plugins/utils.d.ts.map +1 -0
  83. package/build/plugins/utils.js +33 -0
  84. package/build/plugins/utils.js.map +1 -0
  85. package/build/service.d.ts +24 -0
  86. package/build/service.d.ts.map +1 -0
  87. package/build/service.js +95 -0
  88. package/build/service.js.map +1 -0
  89. package/build/types.d.ts +190 -0
  90. package/build/types.d.ts.map +1 -0
  91. package/build/types.js +2 -0
  92. package/build/types.js.map +1 -0
  93. package/build/utils/config.d.ts +13 -0
  94. package/build/utils/config.d.ts.map +1 -0
  95. package/build/utils/config.js +15 -0
  96. package/build/utils/config.js.map +1 -0
  97. package/build/utils/null-report.d.ts +36 -0
  98. package/build/utils/null-report.d.ts.map +1 -0
  99. package/build/utils/null-report.js +84 -0
  100. package/build/utils/null-report.js.map +1 -0
  101. package/build/utils/prompt.d.ts +15 -0
  102. package/build/utils/prompt.d.ts.map +1 -0
  103. package/build/utils/prompt.js +45 -0
  104. package/build/utils/prompt.js.map +1 -0
  105. package/build/utils/schema.d.ts +20 -0
  106. package/build/utils/schema.d.ts.map +1 -0
  107. package/build/utils/schema.js +28 -0
  108. package/build/utils/schema.js.map +1 -0
  109. package/build/utils/stream.d.ts +22 -0
  110. package/build/utils/stream.d.ts.map +1 -0
  111. package/build/utils/stream.js +54 -0
  112. package/build/utils/stream.js.map +1 -0
  113. package/package.json +65 -0
  114. package/src/consts.ts +89 -0
  115. package/src/errors.ts +65 -0
  116. package/src/execution/index.ts +4 -0
  117. package/src/execution/service.ts +185 -0
  118. package/src/execution/types.ts +139 -0
  119. package/src/execution/utils.ts +79 -0
  120. package/src/helpers/index.ts +5 -0
  121. package/src/helpers/json.ts +117 -0
  122. package/src/helpers/messages.ts +12 -0
  123. package/src/helpers/retry.ts +59 -0
  124. package/src/helpers/spectate.ts +67 -0
  125. package/src/index.ts +13 -0
  126. package/src/model.ts +379 -0
  127. package/src/plugins/anthropic.ts +97 -0
  128. package/src/plugins/compatible.ts +62 -0
  129. package/src/plugins/export.ts +6 -0
  130. package/src/plugins/index.ts +53 -0
  131. package/src/plugins/openai.ts +108 -0
  132. package/src/plugins/types.ts +92 -0
  133. package/src/plugins/utils.ts +38 -0
  134. package/src/service.ts +125 -0
  135. package/src/types.ts +214 -0
  136. package/src/utils/config.ts +19 -0
  137. package/src/utils/null-report.ts +126 -0
  138. package/src/utils/prompt.ts +46 -0
  139. package/src/utils/schema.ts +35 -0
  140. package/src/utils/stream.ts +58 -0
  141. package/tests/context.ts +110 -0
  142. package/tests/execution.spec.ts +200 -0
  143. package/tests/helpers.spec.ts +192 -0
  144. package/tests/internals.spec.ts +141 -0
  145. package/tests/model.spec.ts +116 -0
  146. package/tests/plugins.spec.ts +227 -0
  147. package/tsconfig.json +19 -0
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@owlmeans/llm",
3
+ "version": "0.1.14",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b",
8
+ "dev": "sleep 174 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
10
+ "test": "bun test ./tests"
11
+ },
12
+ "main": "build/index.js",
13
+ "module": "build/index.js",
14
+ "types": "build/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./build/index.js",
18
+ "require": "./build/index.js",
19
+ "default": "./build/index.js",
20
+ "module": "./build/index.js",
21
+ "types": "./build/index.d.ts"
22
+ },
23
+ "./plugins": {
24
+ "import": "./build/plugins/export.js",
25
+ "require": "./build/plugins/export.js",
26
+ "default": "./build/plugins/export.js",
27
+ "module": "./build/plugins/export.js",
28
+ "types": "./build/plugins/export.d.ts"
29
+ },
30
+ "./helpers": {
31
+ "import": "./build/helpers/index.js",
32
+ "require": "./build/helpers/index.js",
33
+ "default": "./build/helpers/index.js",
34
+ "module": "./build/helpers/index.js",
35
+ "types": "./build/helpers/index.d.ts"
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@langchain/anthropic": "^1.3.26",
40
+ "@langchain/core": "^1.1.39",
41
+ "@langchain/openai": "^1.4.4",
42
+ "@owlmeans/dep-config": "workspace:*",
43
+ "@owlmeans/test": "^0.1.14",
44
+ "@types/bun": "^1.3.14",
45
+ "@types/node": "^26.1.0",
46
+ "nodemon": "^3.1.14",
47
+ "typescript": "^6.0.3"
48
+ },
49
+ "dependencies": {
50
+ "@anthropic-ai/sdk": "^0.78.0",
51
+ "@owlmeans/basic-ids": "^0.1.14",
52
+ "@owlmeans/context": "^0.1.14",
53
+ "@owlmeans/error": "^0.1.14",
54
+ "@owlmeans/llm-common": "^0.1.14",
55
+ "ajv": "^8.17.1"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "peerDependencies": {
61
+ "@langchain/anthropic": "^1.3.26",
62
+ "@langchain/core": "^1.1.39",
63
+ "@langchain/openai": "^1.4.4"
64
+ }
65
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { ExecutionEffort } from '@owlmeans/llm-common'
2
+ import type { ModelConfigPatch } from '@owlmeans/llm-common'
3
+
4
+ /** Context-service alias for the {@link LlmService} (model factory / registry). */
5
+ export const LLM_SERVICE = 'owlmeans-llm-service'
6
+
7
+ /** Context-service alias for the {@link ExecutionService}. */
8
+ export const EXECUTION_SERVICE = 'owlmeans-llm-execution-service'
9
+
10
+ /** Default number of attempts a single model call makes before giving up. */
11
+ export const DEFAULT_MODEL_RETRIES = 8
12
+
13
+ /**
14
+ * Idle (inactivity) deadline in ms for a streamed response: abort the stream when no
15
+ * new token has arrived within this window. NOT a total cap — the timer is re-armed on
16
+ * every chunk, so long but actively-streaming generations are never aborted. Guards
17
+ * against a provider that accepts the request and then never streams anything (observed
18
+ * with throughput-sorted OpenRouter routing), which would otherwise block forever —
19
+ * `maxRetries` never helps there because the request never errors, it just hangs.
20
+ * Overridable per model via `ModelConfig.streamTimeout`.
21
+ */
22
+ export const MODEL_STREAM_TIMEOUT_MS = 5 * 60 * 1000
23
+
24
+ /**
25
+ * Number of failed attempts after which the retry escalator switches from a role's
26
+ * cheap primary model to its configured `fallback` (stronger) model. With
27
+ * {@link DEFAULT_MODEL_RETRIES} = 8 the primary runs attempts 0..2 and the fallback
28
+ * runs attempts 3..7.
29
+ */
30
+ export const FALLBACK_AFTER_ATTEMPTS = 3
31
+
32
+ /**
33
+ * Output-token ceiling used by the retry escalator when a model config declares no
34
+ * `maxTokensCap`. Deliberately high (192K) — it exceeds many real per-request output
35
+ * limits, which is why a precise cap belongs in the preset: without it a retry can
36
+ * issue a 400 "max_tokens exceeds the model's per-request limit".
37
+ */
38
+ export const DEFAULT_MAX_OUTPUT_CAP = 3 * 64000
39
+
40
+ /** Provider hard limit on prompt-cache breakpoints (Anthropic). */
41
+ export const MAX_CACHE_BREAKPOINTS = 4
42
+
43
+ /**
44
+ * Appended to the prompt of `invoke`/`request` when no message already mentions JSON.
45
+ * Some providers refuse or ignore JSON modes unless the word appears in the prompt;
46
+ * the length guidance keeps a verbose model from padding the object past the output
47
+ * limit and truncating it.
48
+ */
49
+ export const JSON_INSTRUCTION = 'Respond with a single complete and valid JSON object only. '
50
+ + 'Do not wrap it in markdown fences, and do not add any commentary, reasoning, or explanation outside the JSON. '
51
+ + 'Keep string values focused — do not pad them with restated requirements or numbered summaries, '
52
+ + 'so the whole object stays within the output limit and is never truncated.'
53
+
54
+ /**
55
+ * Qwen3-family soft switch that suppresses hidden reasoning. Injected when
56
+ * `ModelConfig.disableThinking` is set; without it those models routinely spend the
57
+ * whole output budget on thinking and return empty content with `finish_reason="length"`.
58
+ */
59
+ export const NO_THINK_DIRECTIVE = '/no_think'
60
+
61
+ /** Tool name used for structured output when a schema carries no usable title/name. */
62
+ export const DEFAULT_TOOL_NAME = 'extract'
63
+
64
+ /** Default effort tier when a policy does not specify one. */
65
+ export const DEFAULT_EFFORT = ExecutionEffort.Standard
66
+
67
+ /**
68
+ * Effort tier → JSON-safe model config bump merged into `LlmService.getModel` overrides.
69
+ * Pure data: an explicit `modelOverride` always wins over this table, and a `roleOverride`
70
+ * is applied before it.
71
+ */
72
+ export const EFFORT_TABLE: Record<ExecutionEffort, ModelConfigPatch> = {
73
+ [ExecutionEffort.Economy]: { maxTokensCap: 16000 },
74
+ [ExecutionEffort.Standard]: {},
75
+ [ExecutionEffort.High]: { maxTokens: 16000, maxTokensCap: 32000 },
76
+ [ExecutionEffort.Max]: { maxTokens: 32000, maxTokensCap: 64000 },
77
+ }
78
+
79
+ /**
80
+ * Execution fields that are collaborators, not state: never copied into a snapshot.
81
+ * A consumer adds its own (e.g. a file-access helper) through
82
+ * `ExecutionServiceOptions.collaboratorKeys`.
83
+ *
84
+ * `state` is in the list because a `TaskExecution` carries its own composed state —
85
+ * without excluding it every `derive`/`escalate`/`withPurpose` would nest another copy.
86
+ */
87
+ export const COLLABORATOR_KEYS: string[] = [
88
+ 'state', 'models', 'model', 'temperatureFactory', 'outputErrors',
89
+ ]
package/src/errors.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { ResilientError } from '@owlmeans/error'
2
+
3
+ export class LlmError extends ResilientError {
4
+ public static override typeName = `Llm${ResilientError.typeName}`
5
+
6
+ constructor(message: string = 'error') {
7
+ super(LlmError.typeName, `llm:${message}`)
8
+ }
9
+ }
10
+
11
+ /**
12
+ * A model call produced something unusable (null/empty content, failed validation,
13
+ * a rejected filter, a stalled stream). **Retryable** — `withRetry` swallows it and
14
+ * escalates to the next attempt.
15
+ */
16
+ export class LlmModelError extends LlmError {
17
+ public static override typeName = `Model${LlmError.typeName}`
18
+
19
+ public retry: number = 0
20
+
21
+ constructor(message: string = 'error') {
22
+ super(`model:${message}`)
23
+ this.type = LlmModelError.typeName
24
+ }
25
+ }
26
+
27
+ /** A model alias has no config, or its config names no provider/secret/plugin. */
28
+ export class LlmMissconfiguredError extends LlmError {
29
+ public static override typeName = `Missconfigured${LlmError.typeName}`
30
+
31
+ constructor(message: string = 'error') {
32
+ super(`missconfigured:${message}`)
33
+ this.type = LlmMissconfiguredError.typeName
34
+ }
35
+ }
36
+
37
+ /** No provider plugin is registered for the requested type / model instance. */
38
+ export class LlmPluginError extends LlmError {
39
+ public static readonly NO_PLUGIN = 'no-plugin'
40
+
41
+ public static override typeName = `Plugin${LlmError.typeName}`
42
+
43
+ constructor(message: string = 'error') {
44
+ super(`plugin:${message}`)
45
+ this.type = LlmPluginError.typeName
46
+ }
47
+ }
48
+
49
+ /** Every attempt failed. `cause` carries the last error, `attempt` the last index. */
50
+ export class LlmRetryExceededError extends LlmError {
51
+ public static override typeName = `RetryExceeded${LlmError.typeName}`
52
+
53
+ public attempt: number = 0
54
+
55
+ constructor(message: string = 'error') {
56
+ super(`retry-exceeded:${message}`)
57
+ this.type = LlmRetryExceededError.typeName
58
+ }
59
+ }
60
+
61
+ ResilientError.registerErrorClass(LlmError)
62
+ ResilientError.registerErrorClass(LlmModelError)
63
+ ResilientError.registerErrorClass(LlmMissconfiguredError)
64
+ ResilientError.registerErrorClass(LlmPluginError)
65
+ ResilientError.registerErrorClass(LlmRetryExceededError)
@@ -0,0 +1,4 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './utils.js'
4
+ export * from './service.js'
@@ -0,0 +1,185 @@
1
+ import { createService } from '@owlmeans/context'
2
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
3
+ import { ExecutionLevel } from '@owlmeans/llm-common'
4
+ import type { ExecutionState, ModelPolicy, TaskExecutionState } from '@owlmeans/llm-common'
5
+ import { COLLABORATOR_KEYS, EXECUTION_SERVICE } from '../consts.js'
6
+ import type { TemperatureFactory } from '../types.js'
7
+ import type {
8
+ Execution, ExecutionPlugin, ExecutionService, ExecutionServiceOptions, ExecutionShape,
9
+ HelperExecution, TaskExecution, WithExecutionService,
10
+ } from './types.js'
11
+ import {
12
+ composeExecState, composeTaskState, effortPatch, freeze, mergeOverride, mergePolicy, resolveRole,
13
+ } from './utils.js'
14
+
15
+ /**
16
+ * Build the execution service implementation WITHOUT registering it as a context
17
+ * service, so a consumer can publish extra methods alongside it (observability
18
+ * factories, domain-specific refinement). Spread it into your own `createService`:
19
+ *
20
+ * ```ts
21
+ * const api = executionServiceApi<MyShape>({ collaboratorKeys: ['files'] }, () => service)
22
+ * const service = createService<MyExecutionService>(alias, {
23
+ * ...api,
24
+ * // delegate to `api`, never to `service`, or you recurse
25
+ * forTask: (parent, input) => api.forTask(parent, { ...input, effort: effortOf(input.mode) }),
26
+ * spectator: (exec, kind) => makeSpectator(exec, kind),
27
+ * } as MyExecutionService)
28
+ * ```
29
+ */
30
+ export const executionServiceApi = <S extends ExecutionShape = ExecutionShape>(
31
+ options: ExecutionServiceOptions,
32
+ self: () => ExecutionService<S>,
33
+ ): ExecutionService<S> => {
34
+ const plugins: ExecutionPlugin[] = []
35
+ const collaboratorKeys = [...COLLABORATOR_KEYS, ...(options.collaboratorKeys ?? [])]
36
+
37
+ /** Recompose the JSON-safe state of a task execution after any refinement. */
38
+ const recompose = <E extends Execution>(exec: E): E => {
39
+ if (exec.level === ExecutionLevel.Task) {
40
+ const task = exec as unknown as TaskExecution
41
+ ;(task as { state: TaskExecutionState }).state = composeTaskState(task, collaboratorKeys)
42
+ }
43
+ return exec
44
+ }
45
+
46
+ const api: ExecutionService<S> = {
47
+
48
+ root: input => freeze({
49
+ ...input,
50
+ level: ExecutionLevel.Project,
51
+ purpose: { ...input.purpose },
52
+ policy: { ...input.policy },
53
+ }) as S['project'],
54
+
55
+ forTask: (parent, input) => {
56
+ const { effort, phase, data, ...extras } = input
57
+ const policy = effort != null
58
+ ? mergePolicy(parent.policy, { effort })
59
+ : { ...parent.policy }
60
+
61
+ // Spreading the parent carries every collaborator and domain field forward; the
62
+ // task's own state is composed afterwards, from the seeded resumable fields.
63
+ const taskExec = {
64
+ ...parent, ...extras, level: ExecutionLevel.Task, purpose: { ...parent.purpose }, policy,
65
+ } as unknown as TaskExecution
66
+ ;(taskExec as { state: TaskExecutionState }).state = composeTaskState({
67
+ ...taskExec,
68
+ state: {
69
+ level: ExecutionLevel.Task,
70
+ purpose: taskExec.purpose,
71
+ policy: taskExec.policy,
72
+ phase,
73
+ data,
74
+ } as TaskExecutionState,
75
+ }, collaboratorKeys)
76
+
77
+ return freeze(taskExec) as S['task']
78
+ },
79
+
80
+ forHelper: (parent, input) => {
81
+ const { role, effort, dedication, ...extras } = input
82
+ const localPolicy = effort != null ? mergePolicy(parent.policy, { effort }) : parent.policy
83
+ const scoped = { ...parent, policy: localPolicy } as S['exec']
84
+
85
+ const helperExec = {
86
+ ...parent,
87
+ ...extras,
88
+ level: ExecutionLevel.Helper,
89
+ purpose: dedication != null
90
+ ? { ...parent.purpose, dedication }
91
+ : { ...parent.purpose },
92
+ policy: localPolicy,
93
+ role: resolveRole(localPolicy, role),
94
+ model: self().model(scoped, role),
95
+ temperatureFactory: self().temperatureFactory(scoped, role),
96
+ } as unknown as HelperExecution
97
+ // A helper is not resumable — drop a parent task's composed state.
98
+ delete (helperExec as { state?: unknown }).state
99
+
100
+ return freeze(helperExec) as S['helper']
101
+ },
102
+
103
+ derive: (exec, patch) => freeze(recompose({ ...exec, ...patch })),
104
+
105
+ withPurpose: (exec, patch) =>
106
+ freeze(recompose({ ...exec, purpose: { ...exec.purpose, ...patch } })),
107
+
108
+ escalate: (exec, patch: Partial<ModelPolicy>) =>
109
+ freeze(recompose({ ...exec, policy: mergePolicy(exec.policy, patch) })),
110
+
111
+ model: (exec, role, override) => {
112
+ const effectiveRole = resolveRole(exec.policy, role ?? (exec as HelperExecution).role)
113
+ const policyOverride = exec.policy.modelOverrides?.[effectiveRole]
114
+ const merged = mergeOverride(effortPatch(exec.policy.effort), policyOverride, override)
115
+ // Strip undefined values so the factory does not see spurious keys.
116
+ const clean = Object.fromEntries(
117
+ Object.entries(merged).filter(([, value]) => value !== undefined)
118
+ ) as typeof merged
119
+
120
+ return exec.models().getModel(effectiveRole, clean)
121
+ },
122
+
123
+ temperatureFactory: (exec, role): TemperatureFactory =>
124
+ temperature =>
125
+ self().model(exec, role, {
126
+ ...(temperature != null ? { temperature } : {}),
127
+ ...(temperature != null && temperature > 0.2 ? { topP: 0.8 } : {}),
128
+ }),
129
+
130
+ use: plugin => {
131
+ plugins.push(plugin)
132
+ },
133
+
134
+ checkpoint: async (exec, key) => {
135
+ if (plugins.length === 0) {
136
+ return
137
+ }
138
+ const state = self().snapshot(exec)
139
+ await Promise.all(plugins.map(plugin => plugin.onCheckpoint?.(state, exec, key)))
140
+ },
141
+
142
+ snapshot: exec => {
143
+ if (exec.level === ExecutionLevel.Task) {
144
+ return freeze({ ...(exec as unknown as TaskExecution).state })
145
+ }
146
+ return freeze(composeExecState(exec, collaboratorKeys))
147
+ },
148
+
149
+ restore: (state: ExecutionState, collaborators = {} as S['collaborators']) => freeze({
150
+ ...state,
151
+ ...collaborators,
152
+ ...(state.level === ExecutionLevel.Task ? { state } : {}),
153
+ }) as S['exec'],
154
+
155
+ } as ExecutionService<S>
156
+
157
+ return api
158
+ }
159
+
160
+ export const makeExecutionService = <S extends ExecutionShape = ExecutionShape>(
161
+ alias: string = EXECUTION_SERVICE,
162
+ options: ExecutionServiceOptions = {},
163
+ ): ExecutionService<S> => {
164
+ const service: ExecutionService<S> = createService<ExecutionService<S>>(
165
+ alias, executionServiceApi<S>(options, () => service) as ExecutionService<S>
166
+ )
167
+
168
+ return service
169
+ }
170
+
171
+ export const appendExecutionService = <
172
+ C extends BasicConfig, T extends BasicContext<C>, S extends ExecutionShape = ExecutionShape
173
+ >(
174
+ ctx: T,
175
+ alias: string = EXECUTION_SERVICE,
176
+ options: ExecutionServiceOptions = {},
177
+ ): T & WithExecutionService<S> => {
178
+ const context = ctx as T & WithExecutionService<S>
179
+
180
+ context.registerService(makeExecutionService<S>(alias, options))
181
+
182
+ context.executions = () => context.service<ExecutionService<S>>(alias)
183
+
184
+ return context
185
+ }
@@ -0,0 +1,139 @@
1
+ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
2
+ import type { InitializedService } from '@owlmeans/context'
3
+ import type {
4
+ ExecutionEffort, ExecutionLevel, ExecutionState, LlmPurpose, ModelConfigOverride,
5
+ ModelPolicy, ModelRole, TaskExecutionState,
6
+ } from '@owlmeans/llm-common'
7
+ import type { LlmService, TemperatureFactory } from '../types.js'
8
+
9
+ /**
10
+ * Runtime execution = serializable {@link ExecutionState} + attached collaborators.
11
+ * A frozen data object with NO behavior of its own — all logic (construct, refine,
12
+ * resolve a model, snapshot, restore) lives on {@link ExecutionService}. Passing it to
13
+ * the next performer creates a NEW object; an execution is immutable between layers.
14
+ *
15
+ * Extend this interface (and {@link ExecutionState}) to carry domain context; every
16
+ * field that is not declared a collaborator travels into the snapshot automatically.
17
+ */
18
+ export interface Execution extends ExecutionState {
19
+ /** Resolver for the model factory — a function so the service can be swapped/cloned. */
20
+ models: () => LlmService
21
+ outputErrors?: boolean
22
+ captureNull?: boolean
23
+ }
24
+
25
+ export interface ProjectExecution extends Execution {
26
+ level: ExecutionLevel.Project
27
+ }
28
+
29
+ export interface TaskExecution extends Execution {
30
+ level: ExecutionLevel.Task
31
+ /** The composed, JSON-safe state — recomposed on every refinement. */
32
+ state: TaskExecutionState
33
+ }
34
+
35
+ export interface HelperExecution extends Execution {
36
+ level: ExecutionLevel.Helper
37
+ role: ModelRole
38
+ /** A model already resolved against the policy (effort + overrides). */
39
+ model: BaseChatModel
40
+ temperatureFactory: TemperatureFactory
41
+ }
42
+
43
+ export interface ProjectExecutionInput {
44
+ models: () => LlmService
45
+ policy: ModelPolicy
46
+ purpose: LlmPurpose
47
+ outputErrors?: boolean
48
+ captureNull?: boolean
49
+ }
50
+
51
+ export interface TaskExecutionInput {
52
+ /** Raise (or lower) the effort tier for this task and everything derived from it. */
53
+ effort?: ExecutionEffort
54
+ /** Optional seeds for the resumable task state. */
55
+ phase?: string
56
+ data?: Record<string, unknown>
57
+ }
58
+
59
+ export interface HelperExecutionInput {
60
+ role: ModelRole
61
+ /** Local effort bump without escalating the whole branch. */
62
+ effort?: ExecutionEffort
63
+ /** Refines `purpose.dedication`. */
64
+ dedication?: string
65
+ }
66
+
67
+ /** Collaborators re-attached to a state that was restored from storage. */
68
+ export interface RestoreCollaborators {
69
+ models?: () => LlmService
70
+ }
71
+
72
+ /**
73
+ * Resilience extension seam. A plugin observes flow boundaries (via
74
+ * {@link ExecutionService.checkpoint}) and can persist/enqueue the JSON-safe
75
+ * {@link ExecutionState}, or supply one back on resume. This package ships NO concrete
76
+ * implementation — with no plugin registered, `checkpoint` is a no-op.
77
+ */
78
+ export interface ExecutionPlugin {
79
+ /** Fired at a flow boundary with a JSON-safe snapshot; persist/enqueue as desired. */
80
+ onCheckpoint?: (state: ExecutionState, exec: Execution, key?: string) => Promise<void>
81
+ /** Resume hook: return a previously persisted state for `key`, or `null`. */
82
+ onRestore?: (key: string) => Promise<ExecutionState | null>
83
+ }
84
+
85
+ /**
86
+ * The set of domain types an {@link ExecutionService} works with. A consumer declares
87
+ * its own shape (extending each member) and instantiates the service generic with it,
88
+ * which keeps the method signatures precise without redeclaring — and therefore without
89
+ * the contravariance problem that narrowing an inherited method signature would cause.
90
+ */
91
+ export interface ExecutionShape {
92
+ exec: Execution
93
+ project: ProjectExecution
94
+ task: TaskExecution
95
+ helper: HelperExecution
96
+ projectInput: ProjectExecutionInput
97
+ taskInput: TaskExecutionInput
98
+ helperInput: HelperExecutionInput
99
+ purpose: LlmPurpose
100
+ collaborators: RestoreCollaborators
101
+ }
102
+
103
+ /**
104
+ * Standard OwlMeans context service. Every construction/refinement method returns a new,
105
+ * `Object.freeze`d object — executions are immutable between performers.
106
+ */
107
+ export interface ExecutionService<S extends ExecutionShape = ExecutionShape> extends InitializedService {
108
+ // Construction / refinement (immutable; each returns a frozen object)
109
+ root: (input: S['projectInput']) => S['project']
110
+ forTask: (parent: S['project'], input: S['taskInput']) => S['task']
111
+ forHelper: (parent: S['exec'], input: S['helperInput']) => S['helper']
112
+ derive: <E extends S['exec']>(exec: E, patch: Partial<E>) => E
113
+ withPurpose: <E extends S['exec']>(exec: E, patch: Partial<S['purpose']>) => E
114
+ escalate: <E extends S['exec']>(exec: E, patch: Partial<ModelPolicy>) => E
115
+
116
+ // Model resolution (policy-aware)
117
+ model: (exec: S['exec'], role?: ModelRole, override?: ModelConfigOverride) => BaseChatModel
118
+ temperatureFactory: (exec: S['exec'], role?: ModelRole) => TemperatureFactory
119
+
120
+ // Resilience
121
+ /** Register a resilience plugin (checkpoint/resume). No-op seam until one is provided. */
122
+ use: (plugin: ExecutionPlugin) => void
123
+ /** Snapshot `exec` and dispatch it to every registered plugin. No-op if none. */
124
+ checkpoint: (exec: S['exec'], key?: string) => Promise<void>
125
+ snapshot: (exec: S['exec']) => ExecutionState
126
+ restore: (state: ExecutionState, collaborators?: S['collaborators']) => S['exec']
127
+ }
128
+
129
+ export interface ExecutionServiceOptions {
130
+ /**
131
+ * Execution fields that are collaborators, not state — excluded from every snapshot.
132
+ * Merged with the package's own {@link COLLABORATOR_KEYS}.
133
+ */
134
+ collaboratorKeys?: string[]
135
+ }
136
+
137
+ export interface WithExecutionService<S extends ExecutionShape = ExecutionShape> {
138
+ executions: () => ExecutionService<S>
139
+ }
@@ -0,0 +1,79 @@
1
+ import { ExecutionLevel } from '@owlmeans/llm-common'
2
+ import type {
3
+ ExecutionEffort, ExecutionState, ModelConfigOverride, ModelConfigPatch,
4
+ ModelPolicy, ModelRole, TaskExecutionState,
5
+ } from '@owlmeans/llm-common'
6
+ import { EFFORT_TABLE } from '../consts.js'
7
+ import type { Execution, TaskExecution } from './types.js'
8
+
9
+ export const freeze = <T extends object>(o: T): Readonly<T> => Object.freeze(o)
10
+
11
+ /** Overlay a partial policy onto a base one. Override maps are merged, not replaced. */
12
+ export const mergePolicy = (base: ModelPolicy, patch: Partial<ModelPolicy>): ModelPolicy => ({
13
+ effort: patch.effort ?? base.effort,
14
+ roleOverrides: patch.roleOverrides != null || base.roleOverrides != null
15
+ ? { ...base.roleOverrides, ...patch.roleOverrides }
16
+ : undefined,
17
+ modelOverrides: patch.modelOverrides != null || base.modelOverrides != null
18
+ ? { ...base.modelOverrides, ...patch.modelOverrides }
19
+ : undefined,
20
+ })
21
+
22
+ /** Apply the policy's role→role remap. */
23
+ export const resolveRole = (policy: ModelPolicy, role: ModelRole): ModelRole =>
24
+ (policy.roleOverrides?.[role] as ModelRole | undefined) ?? role
25
+
26
+ export const effortPatch = (effort: ExecutionEffort): ModelConfigPatch => EFFORT_TABLE[effort]
27
+
28
+ /** Normalize a {@link ModelConfigOverride} (alias or patch) to a patch. */
29
+ export const resolveModelConfig = (override: ModelConfigOverride): ModelConfigPatch =>
30
+ typeof override === 'string' ? { preset: override } : override
31
+
32
+ /**
33
+ * Merge effort < policy.modelOverride < call-site override into a single patch.
34
+ * Any field present in a higher-precedence source wins.
35
+ */
36
+ export const mergeOverride = (
37
+ effortBase: ModelConfigPatch,
38
+ policyOverride: ModelConfigOverride | undefined,
39
+ callOverride: ModelConfigOverride | undefined,
40
+ ): ModelConfigPatch => {
41
+ const policy = policyOverride != null ? resolveModelConfig(policyOverride) : {}
42
+ const call = callOverride != null ? resolveModelConfig(callOverride) : {}
43
+ return { ...effortBase, ...policy, ...call }
44
+ }
45
+
46
+ // --- Serialization ---
47
+
48
+ /**
49
+ * Project an execution down to its JSON-safe state: every own field except the declared
50
+ * collaborators. Domain fields added by a consumer are carried through automatically,
51
+ * which is what lets an extended execution be persisted without extra wiring.
52
+ */
53
+ export const composeExecState = (exec: Execution, collaboratorKeys: string[]): ExecutionState => {
54
+ const state: Record<string, unknown> = {}
55
+ for (const [key, value] of Object.entries(exec)) {
56
+ if (!collaboratorKeys.includes(key)) {
57
+ state[key] = value
58
+ }
59
+ }
60
+ return state as unknown as ExecutionState
61
+ }
62
+
63
+ /**
64
+ * Same as {@link composeExecState}, plus the resumable task fields carried over from the
65
+ * execution's PRIOR state (they live only there — `phase`/`cursor`/`completed`/`data` are
66
+ * advanced by the workflow, not by refinement).
67
+ */
68
+ export const composeTaskState = (exec: TaskExecution, collaboratorKeys: string[]): TaskExecutionState => {
69
+ const base = composeExecState(exec, collaboratorKeys) as TaskExecutionState
70
+ const prior = exec.state ?? ({} as TaskExecutionState)
71
+ return {
72
+ ...base,
73
+ level: ExecutionLevel.Task,
74
+ phase: prior.phase,
75
+ completed: prior.completed,
76
+ cursor: prior.cursor,
77
+ data: prior.data,
78
+ }
79
+ }
@@ -0,0 +1,5 @@
1
+
2
+ export * from './retry.js'
3
+ export * from './json.js'
4
+ export * from './messages.js'
5
+ export * from './spectate.js'