@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.
- package/README.md +193 -0
- package/agent-meta/instructions/llm.instructions.md +66 -0
- package/agent-meta/manifest.json +23 -0
- package/agent-meta/skills/llm/SKILL.md +121 -0
- package/build/consts.d.ts +67 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +76 -0
- package/build/consts.js.map +1 -0
- package/build/errors.d.ts +33 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +52 -0
- package/build/errors.js.map +1 -0
- package/build/execution/index.d.ts +4 -0
- package/build/execution/index.d.ts.map +1 -0
- package/build/execution/index.js +3 -0
- package/build/execution/index.js.map +1 -0
- package/build/execution/service.d.ts +21 -0
- package/build/execution/service.d.ts.map +1 -0
- package/build/execution/service.js +129 -0
- package/build/execution/service.js.map +1 -0
- package/build/execution/types.d.ts +119 -0
- package/build/execution/types.d.ts.map +1 -0
- package/build/execution/types.js +2 -0
- package/build/execution/types.js.map +1 -0
- package/build/execution/utils.d.ts +28 -0
- package/build/execution/utils.d.ts.map +1 -0
- package/build/execution/utils.js +60 -0
- package/build/execution/utils.js.map +1 -0
- package/build/helpers/index.d.ts +5 -0
- package/build/helpers/index.d.ts.map +1 -0
- package/build/helpers/index.js +5 -0
- package/build/helpers/index.js.map +1 -0
- package/build/helpers/json.d.ts +24 -0
- package/build/helpers/json.d.ts.map +1 -0
- package/build/helpers/json.js +119 -0
- package/build/helpers/json.js.map +1 -0
- package/build/helpers/messages.d.ts +10 -0
- package/build/helpers/messages.d.ts.map +1 -0
- package/build/helpers/messages.js +9 -0
- package/build/helpers/messages.js.map +1 -0
- package/build/helpers/retry.d.ts +18 -0
- package/build/helpers/retry.d.ts.map +1 -0
- package/build/helpers/retry.js +57 -0
- package/build/helpers/retry.js.map +1 -0
- package/build/helpers/spectate.d.ts +8 -0
- package/build/helpers/spectate.d.ts.map +1 -0
- package/build/helpers/spectate.js +54 -0
- package/build/helpers/spectate.js.map +1 -0
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +11 -0
- package/build/index.js.map +1 -0
- package/build/model.d.ts +12 -0
- package/build/model.d.ts.map +1 -0
- package/build/model.js +297 -0
- package/build/model.js.map +1 -0
- package/build/plugins/anthropic.d.ts +4 -0
- package/build/plugins/anthropic.d.ts.map +1 -0
- package/build/plugins/anthropic.js +86 -0
- package/build/plugins/anthropic.js.map +1 -0
- package/build/plugins/compatible.d.ts +18 -0
- package/build/plugins/compatible.d.ts.map +1 -0
- package/build/plugins/compatible.js +54 -0
- package/build/plugins/compatible.js.map +1 -0
- package/build/plugins/export.d.ts +6 -0
- package/build/plugins/export.d.ts.map +1 -0
- package/build/plugins/export.js +5 -0
- package/build/plugins/export.js.map +1 -0
- package/build/plugins/index.d.ts +18 -0
- package/build/plugins/index.d.ts.map +1 -0
- package/build/plugins/index.js +42 -0
- package/build/plugins/index.js.map +1 -0
- package/build/plugins/openai.d.ts +28 -0
- package/build/plugins/openai.d.ts.map +1 -0
- package/build/plugins/openai.js +89 -0
- package/build/plugins/openai.js.map +1 -0
- package/build/plugins/types.d.ts +80 -0
- package/build/plugins/types.d.ts.map +1 -0
- package/build/plugins/types.js +2 -0
- package/build/plugins/types.js.map +1 -0
- package/build/plugins/utils.d.ts +27 -0
- package/build/plugins/utils.d.ts.map +1 -0
- package/build/plugins/utils.js +33 -0
- package/build/plugins/utils.js.map +1 -0
- package/build/service.d.ts +24 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +95 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +190 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/config.d.ts +13 -0
- package/build/utils/config.d.ts.map +1 -0
- package/build/utils/config.js +15 -0
- package/build/utils/config.js.map +1 -0
- package/build/utils/null-report.d.ts +36 -0
- package/build/utils/null-report.d.ts.map +1 -0
- package/build/utils/null-report.js +84 -0
- package/build/utils/null-report.js.map +1 -0
- package/build/utils/prompt.d.ts +15 -0
- package/build/utils/prompt.d.ts.map +1 -0
- package/build/utils/prompt.js +45 -0
- package/build/utils/prompt.js.map +1 -0
- package/build/utils/schema.d.ts +20 -0
- package/build/utils/schema.d.ts.map +1 -0
- package/build/utils/schema.js +28 -0
- package/build/utils/schema.js.map +1 -0
- package/build/utils/stream.d.ts +22 -0
- package/build/utils/stream.d.ts.map +1 -0
- package/build/utils/stream.js +54 -0
- package/build/utils/stream.js.map +1 -0
- package/package.json +65 -0
- package/src/consts.ts +89 -0
- package/src/errors.ts +65 -0
- package/src/execution/index.ts +4 -0
- package/src/execution/service.ts +185 -0
- package/src/execution/types.ts +139 -0
- package/src/execution/utils.ts +79 -0
- package/src/helpers/index.ts +5 -0
- package/src/helpers/json.ts +117 -0
- package/src/helpers/messages.ts +12 -0
- package/src/helpers/retry.ts +59 -0
- package/src/helpers/spectate.ts +67 -0
- package/src/index.ts +13 -0
- package/src/model.ts +379 -0
- package/src/plugins/anthropic.ts +97 -0
- package/src/plugins/compatible.ts +62 -0
- package/src/plugins/export.ts +6 -0
- package/src/plugins/index.ts +53 -0
- package/src/plugins/openai.ts +108 -0
- package/src/plugins/types.ts +92 -0
- package/src/plugins/utils.ts +38 -0
- package/src/service.ts +125 -0
- package/src/types.ts +214 -0
- package/src/utils/config.ts +19 -0
- package/src/utils/null-report.ts +126 -0
- package/src/utils/prompt.ts +46 -0
- package/src/utils/schema.ts +35 -0
- package/src/utils/stream.ts +58 -0
- package/tests/context.ts +110 -0
- package/tests/execution.spec.ts +200 -0
- package/tests/helpers.spec.ts +192 -0
- package/tests/internals.spec.ts +141 -0
- package/tests/model.spec.ts +116 -0
- package/tests/plugins.spec.ts +227 -0
- 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,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
|
+
}
|