@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 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.
@@ -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.0",
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",