@shrkcrft/ai 0.1.0-alpha.2 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/ai-request.d.ts +23 -0
  2. package/dist/ai-request.d.ts.map +1 -1
  3. package/dist/delegate/delegate-edit-schema.d.ts +44 -0
  4. package/dist/delegate/delegate-edit-schema.d.ts.map +1 -0
  5. package/dist/delegate/delegate-edit-schema.js +77 -0
  6. package/dist/delegate/parse-delegate-edit.d.ts +46 -0
  7. package/dist/delegate/parse-delegate-edit.d.ts.map +1 -0
  8. package/dist/delegate/parse-delegate-edit.js +128 -0
  9. package/dist/gemini/gemini-provider.d.ts +24 -0
  10. package/dist/gemini/gemini-provider.d.ts.map +1 -0
  11. package/dist/gemini/gemini-provider.js +97 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +9 -0
  15. package/dist/llamacpp/llama-cpp-provider.d.ts +56 -0
  16. package/dist/llamacpp/llama-cpp-provider.d.ts.map +1 -0
  17. package/dist/llamacpp/llama-cpp-provider.js +296 -0
  18. package/dist/llm-hints.d.ts +36 -0
  19. package/dist/llm-hints.d.ts.map +1 -0
  20. package/dist/llm-hints.js +92 -0
  21. package/dist/llm-recommendations.d.ts +72 -0
  22. package/dist/llm-recommendations.d.ts.map +1 -0
  23. package/dist/llm-recommendations.js +188 -0
  24. package/dist/ollama/ollama-provider.d.ts +47 -0
  25. package/dist/ollama/ollama-provider.d.ts.map +1 -0
  26. package/dist/ollama/ollama-provider.js +190 -0
  27. package/dist/pipeline/enhancement-pipeline.d.ts +151 -0
  28. package/dist/pipeline/enhancement-pipeline.d.ts.map +1 -0
  29. package/dist/pipeline/enhancement-pipeline.js +339 -0
  30. package/dist/provider-resolver.d.ts +28 -0
  31. package/dist/provider-resolver.d.ts.map +1 -0
  32. package/dist/provider-resolver.js +80 -0
  33. package/package.json +6 -5
@@ -0,0 +1,339 @@
1
+ import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
2
+ import { AiMessageRole } from "../ai-request.js";
3
+ /**
4
+ * Identifier for a stage in the multi-pass enhancement pipeline.
5
+ *
6
+ * The default Claude-agent-oriented pipeline runs `draft → critique →
7
+ * refine → polish`. Callers may pass a custom stage list to truncate,
8
+ * extend, or rearrange the flow.
9
+ */
10
+ export var EnhancementStageKind;
11
+ (function (EnhancementStageKind) {
12
+ EnhancementStageKind["Draft"] = "draft";
13
+ EnhancementStageKind["Critique"] = "critique";
14
+ EnhancementStageKind["Refine"] = "refine";
15
+ EnhancementStageKind["Polish"] = "polish";
16
+ })(EnhancementStageKind || (EnhancementStageKind = {}));
17
+ /**
18
+ * Multi-pass refinement pipeline that turns a deterministic brief into
19
+ * a denser, more agent-ready artefact by making the LLM critique and
20
+ * rewrite its own work.
21
+ *
22
+ * Design contract:
23
+ * - When no provider is supplied, the pipeline returns the
24
+ * `originalContext` unchanged and flags `deterministicFallback`.
25
+ * The deterministic engine remains the source of truth.
26
+ * - When a provider is supplied, every stage call is retried-once on
27
+ * failure; a permanently-failed stage degrades to the previous
28
+ * stage's output (the pipeline never throws and never produces
29
+ * less than the deterministic input).
30
+ * - Stages compose: a caller can pass a 2-stage `[draft, polish]`
31
+ * pipeline for fast paths, or extend with custom critique prompts
32
+ * for project-specific quality bars.
33
+ *
34
+ * Why a pipeline (vs. a single rich prompt): small local models behave
35
+ * dramatically better when asked to "find the gaps in this draft" than
36
+ * when asked to "write the perfect brief in one shot". The critique
37
+ * pass surfaces vague claims and missing evidence; the refine pass
38
+ * fixes them; the polish pass enforces Claude-agent ergonomics
39
+ * (file:line refs, explicit next commands, terse bullets).
40
+ */
41
+ export class EnhancementPipeline {
42
+ stages;
43
+ constructor(stages) {
44
+ this.stages = stages;
45
+ }
46
+ async run(input, provider, options = {}) {
47
+ if (!provider) {
48
+ return ok({
49
+ finalOutput: input.originalContext,
50
+ stages: [],
51
+ totalUsage: { inputTokens: 0, outputTokens: 0 },
52
+ deterministicFallback: true,
53
+ budgetExhausted: false,
54
+ });
55
+ }
56
+ const cap = options.maxPasses ?? this.stages.length;
57
+ const plan = this.stages.slice(0, Math.max(1, cap));
58
+ const stagesOut = [];
59
+ const totalUsage = { inputTokens: 0, outputTokens: 0 };
60
+ let previous = '';
61
+ let lastCritique;
62
+ let lastGood = input.originalContext;
63
+ const startedAt = Date.now();
64
+ let budgetExhausted = false;
65
+ for (let i = 0; i < plan.length; i += 1) {
66
+ // Wall-clock budget guard: stop before starting a stage we have no time
67
+ // for, and keep the best output produced so far.
68
+ const remaining = options.budgetMs !== undefined ? options.budgetMs - (Date.now() - startedAt) : undefined;
69
+ if (remaining !== undefined && remaining <= MIN_STAGE_BUDGET_MS) {
70
+ budgetExhausted = true;
71
+ break;
72
+ }
73
+ const stage = plan[i];
74
+ const messages = stage.buildMessages({
75
+ originalContext: input.originalContext,
76
+ task: input.task,
77
+ previous,
78
+ lastCritique,
79
+ });
80
+ // Effective per-call timeout = min(configured per-stage, remaining budget).
81
+ const perStageTimeout = effectiveTimeout(options.perStageTimeoutMs, remaining);
82
+ const stageResult = await callOnceWithRetry(provider, {
83
+ messages,
84
+ maxTokens: options.maxTokensPerStage ?? 4096,
85
+ temperature: options.temperature ?? 0.2,
86
+ ...(options.model ? { model: options.model } : {}),
87
+ ...(perStageTimeout !== undefined ? { timeoutMs: perStageTimeout } : {}),
88
+ });
89
+ const onStage = options.onStage;
90
+ if (!stageResult.ok) {
91
+ stagesOut.push({
92
+ kind: stage.kind,
93
+ content: lastGood,
94
+ model: options.model ?? '',
95
+ degraded: true,
96
+ errorMessage: stageResult.error.message,
97
+ });
98
+ if (onStage)
99
+ onStage({ kind: stage.kind, ok: false, pass: i + 1, total: plan.length });
100
+ // Stage failed: keep last-good output but allow the pipeline to
101
+ // continue. A failed `critique` is recoverable (`refine` just
102
+ // gets no critique). A failed `refine` falls back to the prior
103
+ // draft. A failed `polish` returns the refined draft.
104
+ previous = lastGood;
105
+ continue;
106
+ }
107
+ const content = (stageResult.value.content ?? '').trim();
108
+ const usage = stageResult.value.usage ?? {};
109
+ if (typeof usage.inputTokens === 'number')
110
+ totalUsage.inputTokens += usage.inputTokens;
111
+ if (typeof usage.outputTokens === 'number')
112
+ totalUsage.outputTokens += usage.outputTokens;
113
+ stagesOut.push({
114
+ kind: stage.kind,
115
+ content,
116
+ model: stageResult.value.model,
117
+ ...(usage.inputTokens || usage.outputTokens ? { usage } : {}),
118
+ });
119
+ if (stage.kind === EnhancementStageKind.Critique) {
120
+ lastCritique = content;
121
+ // Critique is not a candidate for `finalOutput` — keep the
122
+ // previous draft as the running best.
123
+ }
124
+ else {
125
+ previous = content;
126
+ lastGood = content;
127
+ }
128
+ if (onStage)
129
+ onStage({ kind: stage.kind, ok: true, pass: i + 1, total: plan.length });
130
+ }
131
+ return ok({
132
+ finalOutput: lastGood,
133
+ stages: stagesOut,
134
+ totalUsage,
135
+ deterministicFallback: false,
136
+ budgetExhausted,
137
+ });
138
+ }
139
+ }
140
+ /** Don't start a stage with less than this much budget left (a call needs at
141
+ * least this long to have any chance of returning). */
142
+ const MIN_STAGE_BUDGET_MS = 250;
143
+ /**
144
+ * Effective per-call timeout: the tighter of an explicit per-stage cap and the
145
+ * remaining wall-clock budget. Returns undefined when neither is set.
146
+ */
147
+ function effectiveTimeout(perStage, remaining) {
148
+ const candidates = [perStage, remaining].filter((n) => typeof n === 'number' && n > 0);
149
+ if (candidates.length === 0)
150
+ return undefined;
151
+ return Math.min(...candidates);
152
+ }
153
+ /**
154
+ * The default stage set for "make this brief more useful to the Claude
155
+ * agent". Tuned for small local models (Qwen2.5-Coder-3B, Llama-3.1-8B).
156
+ *
157
+ * Each stage's user message is intentionally short and concrete; the
158
+ * heavy lifting (the deterministic seed) lives in the system role
159
+ * and is reused verbatim across stages so the model never loses
160
+ * grounding.
161
+ */
162
+ export function buildDefaultEnhancementStages() {
163
+ return [
164
+ new DraftStage(),
165
+ new CritiqueStage(),
166
+ new RefineStage(),
167
+ new PolishStage(),
168
+ ];
169
+ }
170
+ /**
171
+ * The fast default for interactive use: `draft → polish` (2 calls). Skips the
172
+ * slow critique + refine round-trip (the two passes small/large local models
173
+ * spend the most wall-clock on) while still applying the polish pass that
174
+ * gives the agent file:line refs and terse imperative bullets. Materially
175
+ * better than a single shot, ~half the calls of the full pipeline. Callers who
176
+ * want maximal density opt into `buildDefaultEnhancementStages()` (the
177
+ * `--plus` path).
178
+ */
179
+ export function buildFastEnhancementStages() {
180
+ return [new DraftStage(), new PolishStage()];
181
+ }
182
+ class DraftStage {
183
+ kind = EnhancementStageKind.Draft;
184
+ buildMessages(input) {
185
+ return [
186
+ {
187
+ role: AiMessageRole.System,
188
+ content: [
189
+ 'You are SharkCraft, a deterministic, local-first code-intelligence engine.',
190
+ 'Your job is to write a concise, Claude-agent-ready brief for the supplied task.',
191
+ 'Treat the repository context below as the ONLY ground truth. Do NOT invent file paths, symbols, or commands.',
192
+ '',
193
+ '## Repository context',
194
+ input.originalContext.trim(),
195
+ ].join('\n'),
196
+ },
197
+ {
198
+ role: AiMessageRole.User,
199
+ content: [
200
+ `# Task`,
201
+ input.task.trim(),
202
+ '',
203
+ '# Write the draft brief',
204
+ 'Sections, in order:',
205
+ '1. **Goal** — one sentence.',
206
+ '2. **Files to read** — bullet list, `path` (no line numbers, just path) with one-line rationale.',
207
+ '3. **Files likely to modify** — bullet list, same format.',
208
+ '4. **Implementation sketch** — 3–6 bullets, imperative.',
209
+ '5. **Risks / unknowns** — bullets; mark each "RISK" or "UNKNOWN".',
210
+ '6. **First commands** — fenced bash, one command per line.',
211
+ '',
212
+ 'Be terse. Skip prose. Skip preambles. Skip "I will now…".',
213
+ ].join('\n'),
214
+ },
215
+ ];
216
+ }
217
+ }
218
+ class CritiqueStage {
219
+ kind = EnhancementStageKind.Critique;
220
+ buildMessages(input) {
221
+ return [
222
+ {
223
+ role: AiMessageRole.System,
224
+ content: [
225
+ 'You are a code-review style critic for SharkCraft briefs.',
226
+ 'Treat the repository context below as the ONLY ground truth.',
227
+ '',
228
+ '## Repository context',
229
+ input.originalContext.trim(),
230
+ ].join('\n'),
231
+ },
232
+ {
233
+ role: AiMessageRole.User,
234
+ content: [
235
+ `# Original task`,
236
+ input.task.trim(),
237
+ '',
238
+ `# Draft brief to critique`,
239
+ input.previous.trim() || '(empty)',
240
+ '',
241
+ '# Critique',
242
+ 'Find concrete issues. For each issue: one line, prefixed with one of:',
243
+ '- `GAP:` — something important the brief omits.',
244
+ '- `VAGUE:` — a claim that lacks an exact file path, symbol, or command.',
245
+ '- `WRONG:` — a claim that contradicts the repository context.',
246
+ '- `MISSING-EVIDENCE:` — a claim with no file:line or knowledge-entry id behind it.',
247
+ '',
248
+ 'If the draft is already strong, output a single line: `OK`.',
249
+ 'Do NOT rewrite the brief. Critique only.',
250
+ ].join('\n'),
251
+ },
252
+ ];
253
+ }
254
+ }
255
+ class RefineStage {
256
+ kind = EnhancementStageKind.Refine;
257
+ buildMessages(input) {
258
+ return [
259
+ {
260
+ role: AiMessageRole.System,
261
+ content: [
262
+ 'You are SharkCraft. Rewrite the draft brief to address the critique, while staying strictly grounded in the repository context.',
263
+ '',
264
+ '## Repository context',
265
+ input.originalContext.trim(),
266
+ ].join('\n'),
267
+ },
268
+ {
269
+ role: AiMessageRole.User,
270
+ content: [
271
+ `# Original task`,
272
+ input.task.trim(),
273
+ '',
274
+ `# Draft brief`,
275
+ input.previous.trim() || '(empty)',
276
+ '',
277
+ `# Critique to address`,
278
+ (input.lastCritique ?? 'OK').trim(),
279
+ '',
280
+ '# Rewrite the brief',
281
+ 'Same section layout as the draft. Resolve every GAP/VAGUE/WRONG/MISSING-EVIDENCE line by adding an exact file path or removing the claim. Keep it terse.',
282
+ ].join('\n'),
283
+ },
284
+ ];
285
+ }
286
+ }
287
+ class PolishStage {
288
+ kind = EnhancementStageKind.Polish;
289
+ buildMessages(input) {
290
+ return [
291
+ {
292
+ role: AiMessageRole.System,
293
+ content: [
294
+ 'You are SharkCraft. Final polish pass — improve readability for an AI coding agent (e.g. Claude Code) that will consume this brief.',
295
+ 'Keep the meaning intact. Do not add new facts.',
296
+ '',
297
+ '## Repository context (reference only — do not extend)',
298
+ input.originalContext.trim(),
299
+ ].join('\n'),
300
+ },
301
+ {
302
+ role: AiMessageRole.User,
303
+ content: [
304
+ `# Original task`,
305
+ input.task.trim(),
306
+ '',
307
+ `# Brief to polish`,
308
+ input.previous.trim() || '(empty)',
309
+ '',
310
+ '# Polish pass',
311
+ 'Rules:',
312
+ '- Convert any `path` reference to `path:lineNumber` when a line number appears in the context (do not invent line numbers).',
313
+ '- Keep each bullet to one line.',
314
+ '- Promote any imperative verb to the start of the bullet (`Add`, `Wire`, `Replace`, …).',
315
+ '- Surface any RISK / UNKNOWN as a short, scannable bullet.',
316
+ '- Output the brief only — no meta commentary, no "Here is the polished version".',
317
+ ].join('\n'),
318
+ },
319
+ ];
320
+ }
321
+ }
322
+ async function callOnceWithRetry(provider, request) {
323
+ const first = await provider.send(request);
324
+ if (first.ok) {
325
+ return ok({ content: first.value.content, model: first.value.model, usage: first.value.usage });
326
+ }
327
+ // Don't retry a timeout — the model is too slow for the budget, so a second
328
+ // attempt just burns another timeout period. Surface the timeout immediately.
329
+ if (first.error.code === ERROR_CODES.TIMEOUT) {
330
+ return first;
331
+ }
332
+ // One retry — small local models routinely 500 on the first request
333
+ // after a daemon start. Idempotent reissue is safe.
334
+ const second = await provider.send(request);
335
+ if (second.ok) {
336
+ return ok({ content: second.value.content, model: second.value.model, usage: second.value.usage });
337
+ }
338
+ return err(new AppErrorImpl(ERROR_CODES.IO_ERROR, `Enhancement-pipeline stage failed twice: ${second.error.message}`, { cause: second.error }));
339
+ }
@@ -0,0 +1,28 @@
1
+ import type { IAiProvider } from './ai-provider.js';
2
+ export type AiProviderKind = 'auto' | 'claude' | 'gemini' | 'ollama' | 'llamacpp';
3
+ /**
4
+ * Resolve an AI provider by kind.
5
+ *
6
+ * The selector is layered so callers can stay terse:
7
+ * - `selectAiProvider('llamacpp' | 'ollama' | 'claude' | 'gemini')`
8
+ * → explicit pick. Returned even when `isReady()` is true; the
9
+ * caller decides what to do with a non-ready provider.
10
+ * - `selectAiProvider('auto')` (or `undefined`) → walk the local-first
11
+ * readiness chain: `llamacpp → ollama`. This is the default for
12
+ * SharkCraft: privacy + offline first, no surprise network calls
13
+ * to hosted APIs.
14
+ *
15
+ * Gemini and Claude are deliberately excluded from the `auto` chain.
16
+ * They are still callable via explicit `--provider gemini` /
17
+ * `--provider claude` (or `AI_PROVIDER=gemini` / `AI_PROVIDER=claude`)
18
+ * for users who keep API keys around — but the system never reaches
19
+ * out to a hosted LLM on its own.
20
+ *
21
+ * An unrecognised kind collapses to `'auto'` so the caller never has
22
+ * to validate user input twice.
23
+ */
24
+ export declare function selectAiProvider(kind?: string): {
25
+ requested: AiProviderKind;
26
+ provider: IAiProvider | null;
27
+ };
28
+ //# sourceMappingURL=provider-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-resolver.d.ts","sourceRoot":"","sources":["../src/provider-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMpD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,CAAC,EAAE,MAAM,GACZ;IAAE,SAAS,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,CAmB7D"}
@@ -0,0 +1,80 @@
1
+ import { ClaudeProvider } from "./claude/claude-provider.js";
2
+ import { GeminiProvider } from "./gemini/gemini-provider.js";
3
+ import { OllamaProvider } from "./ollama/ollama-provider.js";
4
+ import { LlamaCppProvider } from "./llamacpp/llama-cpp-provider.js";
5
+ /**
6
+ * Resolve an AI provider by kind.
7
+ *
8
+ * The selector is layered so callers can stay terse:
9
+ * - `selectAiProvider('llamacpp' | 'ollama' | 'claude' | 'gemini')`
10
+ * → explicit pick. Returned even when `isReady()` is true; the
11
+ * caller decides what to do with a non-ready provider.
12
+ * - `selectAiProvider('auto')` (or `undefined`) → walk the local-first
13
+ * readiness chain: `llamacpp → ollama`. This is the default for
14
+ * SharkCraft: privacy + offline first, no surprise network calls
15
+ * to hosted APIs.
16
+ *
17
+ * Gemini and Claude are deliberately excluded from the `auto` chain.
18
+ * They are still callable via explicit `--provider gemini` /
19
+ * `--provider claude` (or `AI_PROVIDER=gemini` / `AI_PROVIDER=claude`)
20
+ * for users who keep API keys around — but the system never reaches
21
+ * out to a hosted LLM on its own.
22
+ *
23
+ * An unrecognised kind collapses to `'auto'` so the caller never has
24
+ * to validate user input twice.
25
+ */
26
+ export function selectAiProvider(kind) {
27
+ const normalised = normaliseKind(kind);
28
+ if (normalised === 'claude') {
29
+ const provider = new ClaudeProvider();
30
+ return { requested: 'claude', provider: provider.isReady() ? provider : null };
31
+ }
32
+ if (normalised === 'gemini') {
33
+ const provider = new GeminiProvider();
34
+ return { requested: 'gemini', provider: provider.isReady() ? provider : null };
35
+ }
36
+ if (normalised === 'ollama') {
37
+ const provider = new OllamaProvider();
38
+ return { requested: 'ollama', provider: provider.isReady() ? provider : null };
39
+ }
40
+ if (normalised === 'llamacpp') {
41
+ const provider = new LlamaCppProvider();
42
+ return { requested: 'llamacpp', provider: provider.isReady() ? provider : null };
43
+ }
44
+ return autoSelect();
45
+ }
46
+ function normaliseKind(kind) {
47
+ const known = new Set(['claude', 'gemini', 'ollama', 'llamacpp']);
48
+ if (kind !== undefined) {
49
+ const explicit = kind.trim().toLowerCase();
50
+ if (known.has(explicit))
51
+ return explicit;
52
+ }
53
+ const envCandidate = (process.env.AI_PROVIDER ?? '').trim().toLowerCase();
54
+ if (known.has(envCandidate))
55
+ return envCandidate;
56
+ return 'auto';
57
+ }
58
+ function autoSelect() {
59
+ for (const kind of defaultAutoChain()) {
60
+ if (kind === 'llamacpp') {
61
+ const provider = new LlamaCppProvider();
62
+ if (provider.isReady())
63
+ return { requested: 'auto', provider };
64
+ }
65
+ else if (kind === 'ollama') {
66
+ const provider = new OllamaProvider();
67
+ if (provider.isReady())
68
+ return { requested: 'auto', provider };
69
+ }
70
+ }
71
+ return { requested: 'auto', provider: null };
72
+ }
73
+ /**
74
+ * Local-first chain. Hosted providers (Gemini, Claude) are
75
+ * intentionally absent — opting into a hosted API has to be explicit
76
+ * via `--provider <name>` or `AI_PROVIDER=<name>`.
77
+ */
78
+ function defaultAutoChain() {
79
+ return ['llamacpp', 'ollama'];
80
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@shrkcrft/ai",
3
- "version": "0.1.0-alpha.2",
4
- "description": "SharkCraft AI provider abstraction: Claude HTTP + Claude CLI adapters.",
3
+ "version": "0.1.0-alpha.21",
4
+ "description": "SharkCraft local LLM provider abstraction: Ollama (HTTP) + llama.cpp (in-process) + multi-pass enhancement pipeline.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
7
7
  "type": "module",
8
8
  "main": "./dist/index.js",
9
- "types": "./dist/index.d.d.ts",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
@@ -43,8 +43,9 @@
43
43
  "typecheck": "tsc --noEmit -p tsconfig.json"
44
44
  },
45
45
  "dependencies": {
46
- "@shrkcrft/core": "^0.1.0-alpha.2",
47
- "@shrkcrft/context": "^0.1.0-alpha.2"
46
+ "@shrkcrft/core": "^0.1.0-alpha.21",
47
+ "@shrkcrft/context": "^0.1.0-alpha.21",
48
+ "node-llama-cpp": "^3.16.0"
48
49
  },
49
50
  "publishConfig": {
50
51
  "access": "public"