@7n/llm-lib 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +3 -0
- package/lib/docs/harness.md +31 -0
- package/lib/harness.mjs +119 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.0] - 2026-07-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- harness (Фаза A4): createHarness — декларативний фасад над runOneShot/runAgentFix/runAgentSkill (профіль-обʼєкт {schema_version, kind, ...} → делегація в раннер, per-виклик поля перекривають); + subpath-експорти anchored-edit, web-tools
|
|
8
|
+
|
|
3
9
|
## [2.3.0] - 2026-07-11
|
|
4
10
|
|
|
5
11
|
### Added
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ pi свідомо лишає на caller-а: model tiers, fail-fast політи
|
|
|
23
23
|
|
|
24
24
|
| Імпорт | Що дає |
|
|
25
25
|
| --- | --- |
|
|
26
|
+
| `@7n/llm-lib/harness` | `createHarness({profiles})` → `{run(spec), profileNames()}` — декларативний фасад над раннерами (профіль-обʼєкт `{schema_version, kind, ...}` → делегація); `validateProfile(p)`, `HARNESS_SCHEMA_VERSION` |
|
|
26
27
|
| `@7n/llm-lib/one-shot` | `runOneShot({messages, modelTier?, modelSpec?, ...})` → `{content, usage, error, model, caller}` |
|
|
27
28
|
| `@7n/llm-lib/agent-fix` | `runAgentFix(ruleId, violation, cwd, opts)` → `{applied, touchedFiles, telemetry, error, rollback}`; `buildFixPrompt(...)` |
|
|
28
29
|
| `@7n/llm-lib/agent-skill` | `runAgentSkill(prompt, opts)` → `{ok, telemetry, error}` |
|
|
@@ -35,6 +36,8 @@ pi свідомо лишає на caller-а: model tiers, fail-fast політи
|
|
|
35
36
|
| `@7n/llm-lib/with-timeout` | `withTimeout(promise, ms, {onTimeout?, label?})` |
|
|
36
37
|
| `@7n/llm-lib/prompt-budget` | `budgetFor(kind)`, `fitToBudget(chunks, maxChars)`, `packBatch(items, maxChars)`, `capText(text, maxChars)` |
|
|
37
38
|
| `@7n/llm-lib/body-capture` | `captureBody(record, opts?)` (opt-in, `N_LLM_TRACE_BODIES=1`), `bodiesDir()`, `bodyCaptureEnabled()` |
|
|
39
|
+
| `@7n/llm-lib/anchored-edit` | `createAnchoredTools({cwd, defineTool})`, `applyAnchoredEdits(content, edits)`, `lineAnchor(text)`, `renderAnchored(content, range?)` — hash-anchored строгі edit-tools (профіль `anchoredEdits`) |
|
|
40
|
+
| `@7n/llm-lib/web-tools` | `createWebTools({defineTool})`, `fetchPage(url, opts?)`, `assertPublicHttpUrl(url)`, `resolveSearchProvider(env)` — web_search/web_fetch із SSRF-guard (профіль `webTools`) |
|
|
38
41
|
|
|
39
42
|
`lib/internal/` (registry, memory-guard, max-tokens, chain-headers, compress-context,
|
|
40
43
|
apply-compression) — НЕ публічний API: не імпортувати зовні пакета, subpath-експортів
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: harness.mjs
|
|
4
|
+
resource: llm-lib/lib/harness.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 3ff88af8
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Огляд
|
|
10
|
+
|
|
11
|
+
Run-harness фасад (Фаза A4): єдиний декларативний вхід над трьома раннерами пакета (`runOneShot` / `runAgentFix` / `runAgentSkill`). Consumer описує ЩО запустити профілем-обʼєктом, а не набором позиційних opts; той самий профіль серіалізується у JSON — це те, що дозволяє майбутньому MT-адаптеру мапити вузол графа на конфігурацію без коду. Фасад тонкий: резолвить профіль у opts і делегує в наявний раннер, не дублюючи їхньої логіки (write-guard, verify-loop, toolset-и лишаються в раннерах).
|
|
12
|
+
|
|
13
|
+
## Поведінка
|
|
14
|
+
|
|
15
|
+
Профіль — обʼєкт `{ schema_version, kind, …налаштування }`, де `kind` (`fix`/`skill`/`one-shot`) привʼязує його до раннера, а решта полів (tier, model, timeoutMs, maxTokens, thinkingLevel, verifyMax, anchoredEdits, webTools) стають дефолтами opts. `schema_version` присутній з першої версії й перевіряється на сумісність — несумісний або невідомий `kind` дає структуровану помилку валідації ще до раннера.
|
|
16
|
+
|
|
17
|
+
`createHarness({ profiles })` повертає обʼєкт із `run(spec)` і `profileNames()`. У `run` профіль задається іменем (з мапи) або інлайн-обʼєктом, а per-виклик поля (динаміка: cwd, violation, verify, messages, prompt тощо) зливаються поверх дефолтів профілю й перекривають збіжні. Далі harness делегує з правильними позиційними аргументами кожного раннера: `fix` → `(ruleId, violation, cwd, opts)`, `skill` → `(prompt, opts)`, `one-shot` → `(opts)`. Поля `kind`/`schema_version` у opts раннера не потрапляють.
|
|
18
|
+
|
|
19
|
+
Раннери тягнуться lazy (динамічний import у гілці потрібного `kind`) — top-level модуль лишається вільним від pi; у тестах раннери інжектуються через `deps`.
|
|
20
|
+
|
|
21
|
+
## Публічний API
|
|
22
|
+
|
|
23
|
+
HARNESS_SCHEMA_VERSION — поточна версія схеми профілю.
|
|
24
|
+
validateProfile — перевіряє `kind` і `schema_version`, повертає `{ok}` або `{ok:false, error}`.
|
|
25
|
+
createHarness — будує harness із іменованих профілів; `run(spec)` запускає задачу, `profileNames()` перелічує профілі.
|
|
26
|
+
|
|
27
|
+
## Гарантії поведінки
|
|
28
|
+
|
|
29
|
+
- Контракт раннерів не змінюється: harness лише перекладає профіль+виклик у їхні аргументи й повертає їхній результат як є.
|
|
30
|
+
- Невалідний/невідомий профіль зупиняється до виклику раннера (жодного часткового ефекту).
|
|
31
|
+
- Top-level pi-free: жодного pi-import, поки не викликано `run` відповідного kind.
|
package/lib/harness.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** @see ./docs/harness.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run-harness фасад (Фаза A4, дизайн 2026-07-11): єдиний декларативний вхід над
|
|
5
|
+
* трьома раннерами (`runOneShot` / `runAgentFix` / `runAgentSkill`).
|
|
6
|
+
*
|
|
7
|
+
* Мета — щоб consumer (inline-драбина n-cursor, майбутній MT-runner Фази B, 7n-test)
|
|
8
|
+
* описував ЩО запустити **профілем-обʼєктом**, а не набором позиційних opts, і щоб
|
|
9
|
+
* той самий профіль серіалізувався у JSON (Фаза B мапить `a.md`-вузол MT → профіль
|
|
10
|
+
* без коду). Фасад тонкий: він резолвить профіль у opts і делегує в наявний раннер,
|
|
11
|
+
* НЕ дублюючи їхню логіку (write-guard, verify-loop, toolset-и лишаються в раннерах).
|
|
12
|
+
*
|
|
13
|
+
* Профіль (усі поля опційні, крім прив'язки до раннера через `kind`):
|
|
14
|
+
* { schema_version: 1, kind: 'fix'|'skill'|'one-shot',
|
|
15
|
+
* tier, model, timeoutMs, maxTokens, thinkingLevel,
|
|
16
|
+
* verifyMax, anchoredEdits, webTools }
|
|
17
|
+
* `schema_version` присутній з дня 1 — Фаза B хоче стабільності контракту.
|
|
18
|
+
*
|
|
19
|
+
* Модуль pi-free на рівні top-level: раннери самі роблять lazy pi-import у fix/skill
|
|
20
|
+
* гілці; `createHarness` лише готує замикання.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Поточна версія схеми профілю. Несумісна зміна → bump + міграція consumer-ів. */
|
|
24
|
+
export const HARNESS_SCHEMA_VERSION = 1
|
|
25
|
+
|
|
26
|
+
/** Підтримувані види задач (прив'язка профілю до раннера). */
|
|
27
|
+
const KINDS = new Set(['fix', 'skill', 'one-shot'])
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Валідує профіль: відомий `kind`, сумісний `schema_version`.
|
|
31
|
+
* @param {object} profile профіль-обʼєкт
|
|
32
|
+
* @returns {{ ok: true } | { ok: false, error: string }} результат валідації
|
|
33
|
+
*/
|
|
34
|
+
export function validateProfile(profile) {
|
|
35
|
+
if (!profile || typeof profile !== 'object') return { ok: false, error: 'профіль має бути обʼєктом' }
|
|
36
|
+
const v = profile.schema_version ?? HARNESS_SCHEMA_VERSION
|
|
37
|
+
if (v !== HARNESS_SCHEMA_VERSION) {
|
|
38
|
+
return { ok: false, error: `несумісний schema_version ${v} (очікується ${HARNESS_SCHEMA_VERSION})` }
|
|
39
|
+
}
|
|
40
|
+
if (!KINDS.has(profile.kind)) {
|
|
41
|
+
return { ok: false, error: `невідомий kind "${profile.kind}" (допустимі: ${[...KINDS].join(', ')})` }
|
|
42
|
+
}
|
|
43
|
+
return { ok: true }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Резолвить профіль + per-виклик поля у opts конкретного раннера.
|
|
48
|
+
* Профіль задає дефолти конфігурації, `call` — динаміку виклику (cwd, violation,
|
|
49
|
+
* verify, chain тощо); `call` перекриває збіжні поля профілю.
|
|
50
|
+
* @param {object} profile профіль-обʼєкт (валідований)
|
|
51
|
+
* @param {object} call per-виклик поля
|
|
52
|
+
* @returns {object} opts для раннера
|
|
53
|
+
*/
|
|
54
|
+
function resolveOpts(profile, call) {
|
|
55
|
+
const { schema_version: _sv, kind: _kind, ...profileOpts } = profile
|
|
56
|
+
return { ...profileOpts, ...call }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Створює harness із набором іменованих профілів.
|
|
61
|
+
* @param {{ profiles?: Record<string, object>,
|
|
62
|
+
* deps?: { runOneShot?: (opts: object) => Promise<object>, runAgentFix?: (...args: unknown[]) => Promise<object>, runAgentSkill?: (prompt: string, opts: object) => Promise<object> } }} [args]
|
|
63
|
+
* `profiles` — мапа імʼя→профіль; `deps` — інжекція раннерів (тести / кастомний wiring).
|
|
64
|
+
* @returns {{ run: (spec: object) => Promise<object>, profileNames: () => string[] }} harness
|
|
65
|
+
*/
|
|
66
|
+
export function createHarness({ profiles = {}, deps = {} } = {}) {
|
|
67
|
+
/**
|
|
68
|
+
* Лениво тягне раннер (pi-free top-level: імпорт лише при першому виклику потрібного kind).
|
|
69
|
+
* @param {string} kind вид задачі
|
|
70
|
+
* @returns {Promise<(...args: unknown[]) => Promise<object>>} функція-раннер
|
|
71
|
+
*/
|
|
72
|
+
async function runnerFor(kind) {
|
|
73
|
+
if (kind === 'fix') {
|
|
74
|
+
if (deps.runAgentFix) return deps.runAgentFix
|
|
75
|
+
const mod = await import('./agent-fix.mjs')
|
|
76
|
+
return mod.runAgentFix
|
|
77
|
+
}
|
|
78
|
+
if (kind === 'skill') {
|
|
79
|
+
if (deps.runAgentSkill) return deps.runAgentSkill
|
|
80
|
+
const mod = await import('./agent-skill.mjs')
|
|
81
|
+
return mod.runAgentSkill
|
|
82
|
+
}
|
|
83
|
+
if (deps.runOneShot) return deps.runOneShot
|
|
84
|
+
const mod = await import('./one-shot.mjs')
|
|
85
|
+
return mod.runOneShot
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Запускає задачу за профілем.
|
|
90
|
+
* @param {object} spec `{ profile: string|object, ...call }` — профіль за іменем або
|
|
91
|
+
* інлайн-обʼєктом + per-виклик поля (`fix`: ruleId, violation, cwd, verify, targetFiles…;
|
|
92
|
+
* `skill`: prompt, cwd…; `one-shot`: messages…).
|
|
93
|
+
* @returns {Promise<object>} результат відповідного раннера (контракт не змінюється)
|
|
94
|
+
*/
|
|
95
|
+
async function run(spec = {}) {
|
|
96
|
+
const { profile: profileRef, ...call } = spec
|
|
97
|
+
const profile = typeof profileRef === 'string' ? profiles[profileRef] : profileRef
|
|
98
|
+
if (!profile) throw new Error(`профіль не знайдено: ${JSON.stringify(profileRef)}`)
|
|
99
|
+
const valid = validateProfile(profile)
|
|
100
|
+
if (!valid.ok) throw new Error(`невалідний профіль: ${valid.error}`)
|
|
101
|
+
|
|
102
|
+
const run = await runnerFor(profile.kind)
|
|
103
|
+
const opts = resolveOpts(profile, call)
|
|
104
|
+
if (profile.kind === 'fix') {
|
|
105
|
+
// runAgentFix(ruleId, violation, cwd, opts) — позиційні + opts.
|
|
106
|
+
const { ruleId, violation, cwd, ...rest } = opts
|
|
107
|
+
return run(ruleId, violation, cwd, rest)
|
|
108
|
+
}
|
|
109
|
+
if (profile.kind === 'skill') {
|
|
110
|
+
// runAgentSkill(prompt, opts) — prompt позиційний.
|
|
111
|
+
const { prompt, ...rest } = opts
|
|
112
|
+
return run(prompt, rest)
|
|
113
|
+
}
|
|
114
|
+
// one-shot: усе в одному obj-arg.
|
|
115
|
+
return run(opts)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { run, profileNames: () => Object.keys(profiles) }
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/llm-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Тонкий шар роботи з LLM (локальні omlx + хмарні провайдери) поверх pi: model tiers, one-shot, agentic-раннери, write-guard, trace, telemetry, prompt-budget",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nitra",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"!**/*.test.mjs"
|
|
33
33
|
],
|
|
34
34
|
"exports": {
|
|
35
|
+
"./harness": "./lib/harness.mjs",
|
|
36
|
+
"./anchored-edit": "./lib/anchored-edit.mjs",
|
|
37
|
+
"./web-tools": "./lib/web-tools.mjs",
|
|
35
38
|
"./model-tiers": "./lib/model-tiers.mjs",
|
|
36
39
|
"./chain": "./lib/chain.mjs",
|
|
37
40
|
"./chains-report": "./lib/chains-report.mjs",
|