@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.
- package/dist/ai-request.d.ts +23 -0
- package/dist/ai-request.d.ts.map +1 -1
- package/dist/delegate/delegate-edit-schema.d.ts +44 -0
- package/dist/delegate/delegate-edit-schema.d.ts.map +1 -0
- package/dist/delegate/delegate-edit-schema.js +77 -0
- package/dist/delegate/parse-delegate-edit.d.ts +46 -0
- package/dist/delegate/parse-delegate-edit.d.ts.map +1 -0
- package/dist/delegate/parse-delegate-edit.js +128 -0
- package/dist/gemini/gemini-provider.d.ts +24 -0
- package/dist/gemini/gemini-provider.d.ts.map +1 -0
- package/dist/gemini/gemini-provider.js +97 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/llamacpp/llama-cpp-provider.d.ts +56 -0
- package/dist/llamacpp/llama-cpp-provider.d.ts.map +1 -0
- package/dist/llamacpp/llama-cpp-provider.js +296 -0
- package/dist/llm-hints.d.ts +36 -0
- package/dist/llm-hints.d.ts.map +1 -0
- package/dist/llm-hints.js +92 -0
- package/dist/llm-recommendations.d.ts +72 -0
- package/dist/llm-recommendations.d.ts.map +1 -0
- package/dist/llm-recommendations.js +188 -0
- package/dist/ollama/ollama-provider.d.ts +47 -0
- package/dist/ollama/ollama-provider.d.ts.map +1 -0
- package/dist/ollama/ollama-provider.js +190 -0
- package/dist/pipeline/enhancement-pipeline.d.ts +151 -0
- package/dist/pipeline/enhancement-pipeline.d.ts.map +1 -0
- package/dist/pipeline/enhancement-pipeline.js +339 -0
- package/dist/provider-resolver.d.ts +28 -0
- package/dist/provider-resolver.d.ts.map +1 -0
- package/dist/provider-resolver.js +80 -0
- package/package.json +6 -5
package/dist/ai-request.d.ts
CHANGED
|
@@ -13,6 +13,24 @@ export interface IAiRequest {
|
|
|
13
13
|
maxTokens?: number;
|
|
14
14
|
temperature?: number;
|
|
15
15
|
context?: string;
|
|
16
|
+
responseFormat?: IAiResponseFormat;
|
|
17
|
+
/**
|
|
18
|
+
* Optional callback invoked with each newly-decoded token chunk as
|
|
19
|
+
* generation streams in. Providers that don't natively stream
|
|
20
|
+
* (HTTP Gemini / Claude in our non-SSE adapter) ignore this; the
|
|
21
|
+
* llamacpp provider forwards chunks live. Useful for stderr "live
|
|
22
|
+
* preview" in CLI commands and for an agent who wants to display
|
|
23
|
+
* progress without the synchronous wait.
|
|
24
|
+
*/
|
|
25
|
+
onTokenStream?: (chunk: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Per-call wall-clock timeout in milliseconds. When set and exceeded, the
|
|
28
|
+
* provider aborts the in-flight request and returns an `AppError` with code
|
|
29
|
+
* `TIMEOUT`. Bounds slow local models so a single call can never hang the
|
|
30
|
+
* command. Takes precedence over the provider's `config.timeoutMs`; when
|
|
31
|
+
* neither is set, no timeout is applied.
|
|
32
|
+
*/
|
|
33
|
+
timeoutMs?: number;
|
|
16
34
|
}
|
|
17
35
|
export interface IAiResponse {
|
|
18
36
|
content: string;
|
|
@@ -30,4 +48,9 @@ export interface IAiProviderConfig {
|
|
|
30
48
|
model?: string;
|
|
31
49
|
timeoutMs?: number;
|
|
32
50
|
}
|
|
51
|
+
export interface IAiResponseFormat {
|
|
52
|
+
type: 'json_object' | 'json_schema';
|
|
53
|
+
schema?: Record<string, unknown>;
|
|
54
|
+
schemaName?: string;
|
|
55
|
+
}
|
|
33
56
|
//# sourceMappingURL=ai-request.d.ts.map
|
package/dist/ai-request.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-request.d.ts","sourceRoot":"","sources":["../src/ai-request.ts"],"names":[],"mappings":"AAAA,oBAAY,aAAa;IACvB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,SAAS,cAAc;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ai-request.d.ts","sourceRoot":"","sources":["../src/ai-request.ts"],"names":[],"mappings":"AAAA,oBAAY,aAAa;IACvB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,SAAS,cAAc;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,iBAAiB,CAAC;IACnC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,GAAG,aAAa,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structured edit a delegate worker (local LLM) is asked to emit.
|
|
3
|
+
*
|
|
4
|
+
* This layer (`@shrkcrft/ai`) sits BELOW `@shrkcrft/generator`, so it cannot
|
|
5
|
+
* reference `IPlannedOperation`. The worker's output is therefore parsed into a
|
|
6
|
+
* deliberately-RAW shape here: `operation` is `{ kind } & Record<string,
|
|
7
|
+
* unknown>`. The generator-layer packager (`packageDelegatePlan`) is the
|
|
8
|
+
* authority that validates each raw op against the real operation union and the
|
|
9
|
+
* recipe's `allowedOps` — a raw op never writes anything on its own.
|
|
10
|
+
*/
|
|
11
|
+
/** One raw operation the worker wants to apply to a file. */
|
|
12
|
+
export interface IDelegateRawOp {
|
|
13
|
+
/** File path the op targets, relative to the project root. */
|
|
14
|
+
targetPath: string;
|
|
15
|
+
/**
|
|
16
|
+
* The operation intent. `kind` selects an `IPlannedOperation` variant; the
|
|
17
|
+
* remaining fields are validated against that variant downstream, never here.
|
|
18
|
+
*/
|
|
19
|
+
operation: {
|
|
20
|
+
kind: string;
|
|
21
|
+
} & Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
/** The full structured edit returned by a delegate worker. */
|
|
24
|
+
export interface IDelegateRawEdit {
|
|
25
|
+
ops: readonly IDelegateRawOp[];
|
|
26
|
+
/** Optional free-form note from the worker. Informational only; not applied. */
|
|
27
|
+
note?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `IPlannedOperation` kinds a delegate worker may emit. Hardcoded here (not
|
|
31
|
+
* imported from the higher generator layer) — this is a HINT for the model's
|
|
32
|
+
* `json_schema` response format, not the security boundary. The generator's
|
|
33
|
+
* `packageDelegatePlan` + the recipe's `allowedOps` are the real gate, so a
|
|
34
|
+
* kind missing here only makes the model less likely to emit it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DELEGATE_OP_KINDS: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* JSON Schema handed to the provider as `responseFormat.schema` so a local
|
|
39
|
+
* model returns a parseable edit. `operation` allows extra properties on
|
|
40
|
+
* purpose — different op kinds carry different fields, and the generator
|
|
41
|
+
* validates the exact shape per kind.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DELEGATE_EDIT_JSON_SCHEMA: Record<string, unknown>;
|
|
44
|
+
//# sourceMappingURL=delegate-edit-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delegate-edit-schema.d.ts","sourceRoot":"","sources":["../../src/delegate/delegate-edit-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvD;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/B,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAa9C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAuC7D,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structured edit a delegate worker (local LLM) is asked to emit.
|
|
3
|
+
*
|
|
4
|
+
* This layer (`@shrkcrft/ai`) sits BELOW `@shrkcrft/generator`, so it cannot
|
|
5
|
+
* reference `IPlannedOperation`. The worker's output is therefore parsed into a
|
|
6
|
+
* deliberately-RAW shape here: `operation` is `{ kind } & Record<string,
|
|
7
|
+
* unknown>`. The generator-layer packager (`packageDelegatePlan`) is the
|
|
8
|
+
* authority that validates each raw op against the real operation union and the
|
|
9
|
+
* recipe's `allowedOps` — a raw op never writes anything on its own.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* `IPlannedOperation` kinds a delegate worker may emit. Hardcoded here (not
|
|
13
|
+
* imported from the higher generator layer) — this is a HINT for the model's
|
|
14
|
+
* `json_schema` response format, not the security boundary. The generator's
|
|
15
|
+
* `packageDelegatePlan` + the recipe's `allowedOps` are the real gate, so a
|
|
16
|
+
* kind missing here only makes the model less likely to emit it.
|
|
17
|
+
*/
|
|
18
|
+
export const DELEGATE_OP_KINDS = [
|
|
19
|
+
'create',
|
|
20
|
+
'append',
|
|
21
|
+
'insert-after',
|
|
22
|
+
'insert-before',
|
|
23
|
+
'replace',
|
|
24
|
+
'export',
|
|
25
|
+
'ensure-import',
|
|
26
|
+
'insert-enum-entry',
|
|
27
|
+
'insert-object-entry',
|
|
28
|
+
'insert-array-entry',
|
|
29
|
+
'insert-before-closing-brace',
|
|
30
|
+
'insert-between-anchors',
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* JSON Schema handed to the provider as `responseFormat.schema` so a local
|
|
34
|
+
* model returns a parseable edit. `operation` allows extra properties on
|
|
35
|
+
* purpose — different op kinds carry different fields, and the generator
|
|
36
|
+
* validates the exact shape per kind.
|
|
37
|
+
*/
|
|
38
|
+
export const DELEGATE_EDIT_JSON_SCHEMA = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
required: ['ops'],
|
|
42
|
+
properties: {
|
|
43
|
+
note: { type: 'string' },
|
|
44
|
+
ops: {
|
|
45
|
+
type: 'array',
|
|
46
|
+
items: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
required: ['targetPath', 'operation'],
|
|
50
|
+
properties: {
|
|
51
|
+
targetPath: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'file path relative to the project root',
|
|
54
|
+
},
|
|
55
|
+
operation: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: true,
|
|
58
|
+
required: ['kind'],
|
|
59
|
+
properties: {
|
|
60
|
+
kind: { type: 'string', enum: [...DELEGATE_OP_KINDS] },
|
|
61
|
+
from: { type: 'string', description: 'module specifier (export / ensure-import)' },
|
|
62
|
+
symbols: { type: 'array', items: { type: 'string' } },
|
|
63
|
+
typeOnly: { type: 'boolean' },
|
|
64
|
+
find: { type: 'string' },
|
|
65
|
+
replaceWith: { type: 'string' },
|
|
66
|
+
expectMatches: { type: 'number' },
|
|
67
|
+
content: { type: 'string' },
|
|
68
|
+
snippet: { type: 'string' },
|
|
69
|
+
anchor: { type: 'string' },
|
|
70
|
+
ifMissing: { type: 'string' },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + (lightly) validate the JSON a delegate worker emits, and a one-shot
|
|
3
|
+
* generate→parse→reprompt-once helper. Mirrors the smart-context provider call
|
|
4
|
+
* pattern (`callProviderWithRetry`): a PARSE failure reprompts once; a provider
|
|
5
|
+
* / TIMEOUT error surfaces immediately (the orchestrator owns macro-retries).
|
|
6
|
+
*/
|
|
7
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
8
|
+
import type { IAiProvider } from '../ai-provider.js';
|
|
9
|
+
import { type IAiMessage } from '../ai-request.js';
|
|
10
|
+
import { type IDelegateRawEdit } from './delegate-edit-schema.js';
|
|
11
|
+
/** Parse a worker's raw output into a structurally-validated `IDelegateRawEdit`. */
|
|
12
|
+
export declare function parseDelegateEdit(raw: string): Result<IDelegateRawEdit, AppError>;
|
|
13
|
+
export interface IDelegateCallInput {
|
|
14
|
+
provider: IAiProvider;
|
|
15
|
+
messages: readonly IAiMessage[];
|
|
16
|
+
model?: string;
|
|
17
|
+
/** Per-call wall-clock budget; a TIMEOUT surfaces immediately (no retry). */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
maxTokens?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Build the reprompt messages after a PARSE failure. Receives the model's bad
|
|
22
|
+
* output + the parse error. When omitted, a parse failure is returned as-is.
|
|
23
|
+
*/
|
|
24
|
+
reprompt?: (badOutput: string, error: AppError) => readonly IAiMessage[];
|
|
25
|
+
}
|
|
26
|
+
export interface IDelegateCallResult {
|
|
27
|
+
edit: IDelegateRawEdit;
|
|
28
|
+
/** The raw model output that parsed (for telemetry / hand-back). */
|
|
29
|
+
raw: string;
|
|
30
|
+
model: string;
|
|
31
|
+
usage?: {
|
|
32
|
+
inputTokens?: number;
|
|
33
|
+
outputTokens?: number;
|
|
34
|
+
};
|
|
35
|
+
/** True when the first output failed to parse and the reprompt succeeded. */
|
|
36
|
+
retried: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Generate a delegate edit, parsing the output. A provider / TIMEOUT error
|
|
40
|
+
* surfaces immediately; a PARSE failure reprompts ONCE (when a reprompt builder
|
|
41
|
+
* is supplied) before giving up.
|
|
42
|
+
*/
|
|
43
|
+
export declare function callDelegateWithRetry(input: IDelegateCallInput): Promise<Result<IDelegateCallResult, AppError>>;
|
|
44
|
+
/** Convenience for callers building reprompt messages. */
|
|
45
|
+
export declare function delegateRepromptMessage(badOutput: string, error: AppError): IAiMessage;
|
|
46
|
+
//# sourceMappingURL=parse-delegate-edit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-delegate-edit.d.ts","sourceRoot":"","sources":["../../src/delegate/parse-delegate-edit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAKL,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAA6B,KAAK,gBAAgB,EAAuB,MAAM,2BAA2B,CAAC;AAgBlH,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CA0CjF;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,SAAS,UAAU,EAAE,CAAC;CAC1E;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;CAClB;AAkBD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC,CA0BhD;AAED,0DAA0D;AAC1D,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,UAAU,CAQtF"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + (lightly) validate the JSON a delegate worker emits, and a one-shot
|
|
3
|
+
* generate→parse→reprompt-once helper. Mirrors the smart-context provider call
|
|
4
|
+
* pattern (`callProviderWithRetry`): a PARSE failure reprompts once; a provider
|
|
5
|
+
* / TIMEOUT error surfaces immediately (the orchestrator owns macro-retries).
|
|
6
|
+
*/
|
|
7
|
+
import { AppErrorImpl, ERROR_CODES, err, ok, } from '@shrkcrft/core';
|
|
8
|
+
import { AiMessageRole } from "../ai-request.js";
|
|
9
|
+
import { DELEGATE_EDIT_JSON_SCHEMA } from "./delegate-edit-schema.js";
|
|
10
|
+
function invalid(message, cause) {
|
|
11
|
+
return new AppErrorImpl(ERROR_CODES.INVALID_INPUT, message, cause !== undefined ? { cause } : undefined);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Strip a leading/trailing markdown code fence (```json … ```), which weak
|
|
15
|
+
* local models often wrap JSON in despite a json_schema response format.
|
|
16
|
+
*/
|
|
17
|
+
function stripFences(text) {
|
|
18
|
+
const fence = /^\s*```(?:json)?\s*\n([\s\S]*?)\n```\s*$/;
|
|
19
|
+
const m = text.match(fence);
|
|
20
|
+
return m ? m[1] : text;
|
|
21
|
+
}
|
|
22
|
+
/** Parse a worker's raw output into a structurally-validated `IDelegateRawEdit`. */
|
|
23
|
+
export function parseDelegateEdit(raw) {
|
|
24
|
+
const text = stripFences(raw).trim();
|
|
25
|
+
if (text.length === 0)
|
|
26
|
+
return err(invalid('delegate edit is empty'));
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(text);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return err(invalid('delegate edit is not valid JSON', e));
|
|
33
|
+
}
|
|
34
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
35
|
+
return err(invalid('delegate edit must be a JSON object'));
|
|
36
|
+
}
|
|
37
|
+
const obj = parsed;
|
|
38
|
+
const opsRaw = obj['ops'];
|
|
39
|
+
if (!Array.isArray(opsRaw))
|
|
40
|
+
return err(invalid('delegate edit "ops" must be an array'));
|
|
41
|
+
const ops = [];
|
|
42
|
+
for (let i = 0; i < opsRaw.length; i += 1) {
|
|
43
|
+
const o = opsRaw[i];
|
|
44
|
+
if (!o || typeof o !== 'object' || Array.isArray(o)) {
|
|
45
|
+
return err(invalid(`ops[${i}] must be an object`));
|
|
46
|
+
}
|
|
47
|
+
const oo = o;
|
|
48
|
+
const targetPath = oo['targetPath'];
|
|
49
|
+
if (typeof targetPath !== 'string' || targetPath.trim().length === 0) {
|
|
50
|
+
return err(invalid(`ops[${i}].targetPath must be a non-empty string`));
|
|
51
|
+
}
|
|
52
|
+
const operation = oo['operation'];
|
|
53
|
+
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
|
|
54
|
+
return err(invalid(`ops[${i}].operation must be an object`));
|
|
55
|
+
}
|
|
56
|
+
const kind = operation['kind'];
|
|
57
|
+
if (typeof kind !== 'string' || kind.length === 0) {
|
|
58
|
+
return err(invalid(`ops[${i}].operation.kind must be a non-empty string`));
|
|
59
|
+
}
|
|
60
|
+
ops.push({
|
|
61
|
+
targetPath,
|
|
62
|
+
operation: operation,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const edit = { ops };
|
|
66
|
+
if (typeof obj['note'] === 'string')
|
|
67
|
+
edit.note = obj['note'];
|
|
68
|
+
return ok(edit);
|
|
69
|
+
}
|
|
70
|
+
async function sendOnce(input, messages) {
|
|
71
|
+
if (input.model)
|
|
72
|
+
input.provider.configure({ model: input.model });
|
|
73
|
+
const res = await input.provider.send({
|
|
74
|
+
messages,
|
|
75
|
+
...(input.model ? { model: input.model } : {}),
|
|
76
|
+
...(input.maxTokens ? { maxTokens: input.maxTokens } : {}),
|
|
77
|
+
...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
|
|
78
|
+
responseFormat: { type: 'json_schema', schema: DELEGATE_EDIT_JSON_SCHEMA, schemaName: 'DelegateEdit' },
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok)
|
|
81
|
+
return res;
|
|
82
|
+
return ok({ content: res.value.content, model: res.value.model, ...(res.value.usage ? { usage: res.value.usage } : {}) });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Generate a delegate edit, parsing the output. A provider / TIMEOUT error
|
|
86
|
+
* surfaces immediately; a PARSE failure reprompts ONCE (when a reprompt builder
|
|
87
|
+
* is supplied) before giving up.
|
|
88
|
+
*/
|
|
89
|
+
export async function callDelegateWithRetry(input) {
|
|
90
|
+
const first = await sendOnce(input, input.messages);
|
|
91
|
+
if (!first.ok)
|
|
92
|
+
return first;
|
|
93
|
+
const parsed = parseDelegateEdit(first.value.content);
|
|
94
|
+
if (parsed.ok) {
|
|
95
|
+
return ok({
|
|
96
|
+
edit: parsed.value,
|
|
97
|
+
raw: first.value.content,
|
|
98
|
+
model: first.value.model,
|
|
99
|
+
...(first.value.usage ? { usage: first.value.usage } : {}),
|
|
100
|
+
retried: false,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (!input.reprompt)
|
|
104
|
+
return err(parsed.error);
|
|
105
|
+
const retryMessages = input.reprompt(first.value.content, parsed.error);
|
|
106
|
+
const second = await sendOnce(input, retryMessages);
|
|
107
|
+
if (!second.ok)
|
|
108
|
+
return second;
|
|
109
|
+
const reparsed = parseDelegateEdit(second.value.content);
|
|
110
|
+
if (!reparsed.ok)
|
|
111
|
+
return err(reparsed.error);
|
|
112
|
+
return ok({
|
|
113
|
+
edit: reparsed.value,
|
|
114
|
+
raw: second.value.content,
|
|
115
|
+
model: second.value.model,
|
|
116
|
+
...(second.value.usage ? { usage: second.value.usage } : {}),
|
|
117
|
+
retried: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Convenience for callers building reprompt messages. */
|
|
121
|
+
export function delegateRepromptMessage(badOutput, error) {
|
|
122
|
+
return {
|
|
123
|
+
role: AiMessageRole.User,
|
|
124
|
+
content: `Your previous reply could not be parsed: ${error.message}\n` +
|
|
125
|
+
`It must be a single JSON object matching the schema — no prose, no markdown fences.\n` +
|
|
126
|
+
`Previous reply was:\n${badOutput.slice(0, 2000)}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
2
|
+
import { AbstractAiProvider } from '../ai-provider.js';
|
|
3
|
+
import { type IAiRequest, type IAiResponse } from '../ai-request.js';
|
|
4
|
+
/**
|
|
5
|
+
* HTTP adapter for Google's Gemini (Generative Language API).
|
|
6
|
+
*
|
|
7
|
+
* Reads `GEMINI_API_KEY` from env (or `IAiProviderConfig.apiKey`). When
|
|
8
|
+
* the key is missing `isReady()` returns false and `send()` reports an
|
|
9
|
+
* actionable error — same contract as `ClaudeProvider`.
|
|
10
|
+
*
|
|
11
|
+
* The Gemini REST surface differs from Anthropic's: system messages are
|
|
12
|
+
* passed as a top-level `systemInstruction`, conversation turns become
|
|
13
|
+
* `contents[]` with roles `user`/`model`, and the response token cap is
|
|
14
|
+
* `generationConfig.maxOutputTokens` (not `max_tokens`). This adapter
|
|
15
|
+
* translates the provider-neutral `IAiRequest` shape into that wire
|
|
16
|
+
* format and back.
|
|
17
|
+
*/
|
|
18
|
+
export declare class GeminiProvider extends AbstractAiProvider {
|
|
19
|
+
readonly id = "gemini";
|
|
20
|
+
readonly name = "Google Gemini (HTTP)";
|
|
21
|
+
isReady(): boolean;
|
|
22
|
+
send(request: IAiRequest): Promise<Result<IAiResponse, AppError>>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=gemini-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini-provider.d.ts","sourceRoot":"","sources":["../../src/gemini/gemini-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAkC,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAErG;;;;;;;;;;;;;GAaG;AACH,qBAAa,cAAe,SAAQ,kBAAkB;IACpD,QAAQ,CAAC,EAAE,YAAY;IACvB,QAAQ,CAAC,IAAI,0BAA0B;IAEvC,OAAO,IAAI,OAAO;IAIZ,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;CAyExE"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
|
|
2
|
+
import { AbstractAiProvider } from "../ai-provider.js";
|
|
3
|
+
import { AiMessageRole } from "../ai-request.js";
|
|
4
|
+
/**
|
|
5
|
+
* HTTP adapter for Google's Gemini (Generative Language API).
|
|
6
|
+
*
|
|
7
|
+
* Reads `GEMINI_API_KEY` from env (or `IAiProviderConfig.apiKey`). When
|
|
8
|
+
* the key is missing `isReady()` returns false and `send()` reports an
|
|
9
|
+
* actionable error — same contract as `ClaudeProvider`.
|
|
10
|
+
*
|
|
11
|
+
* The Gemini REST surface differs from Anthropic's: system messages are
|
|
12
|
+
* passed as a top-level `systemInstruction`, conversation turns become
|
|
13
|
+
* `contents[]` with roles `user`/`model`, and the response token cap is
|
|
14
|
+
* `generationConfig.maxOutputTokens` (not `max_tokens`). This adapter
|
|
15
|
+
* translates the provider-neutral `IAiRequest` shape into that wire
|
|
16
|
+
* format and back.
|
|
17
|
+
*/
|
|
18
|
+
export class GeminiProvider extends AbstractAiProvider {
|
|
19
|
+
id = 'gemini';
|
|
20
|
+
name = 'Google Gemini (HTTP)';
|
|
21
|
+
isReady() {
|
|
22
|
+
return Boolean(this.config.apiKey ?? process.env.GEMINI_API_KEY);
|
|
23
|
+
}
|
|
24
|
+
async send(request) {
|
|
25
|
+
const apiKey = this.config.apiKey ?? process.env.GEMINI_API_KEY;
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
return err(new AppErrorImpl(ERROR_CODES.INVALID_INPUT, 'GEMINI_API_KEY is not set — cannot reach Gemini', { suggestion: 'Put GEMINI_API_KEY=... in .env or `export GEMINI_API_KEY=...`' }));
|
|
28
|
+
}
|
|
29
|
+
const baseUrl = this.config.baseUrl ?? 'https://generativelanguage.googleapis.com';
|
|
30
|
+
const model = request.model ?? this.config.model ?? 'gemini-2.5-flash';
|
|
31
|
+
const maxTokens = request.maxTokens ?? 4096;
|
|
32
|
+
const systemInstructionText = collectSystem(request.messages);
|
|
33
|
+
const contents = collectContents(request.messages);
|
|
34
|
+
const body = {
|
|
35
|
+
contents,
|
|
36
|
+
generationConfig: {
|
|
37
|
+
maxOutputTokens: maxTokens,
|
|
38
|
+
...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
|
|
39
|
+
...(request.responseFormat
|
|
40
|
+
? {
|
|
41
|
+
responseMimeType: 'application/json',
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
if (systemInstructionText) {
|
|
47
|
+
body.systemInstruction = { role: 'system', parts: [{ text: systemInstructionText }] };
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const url = `${baseUrl}/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
|
|
51
|
+
const res = await fetch(url, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'content-type': 'application/json' },
|
|
54
|
+
body: JSON.stringify(body),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
return err(new AppErrorImpl(ERROR_CODES.IO_ERROR, `Gemini API ${res.status}: ${text.slice(0, 500)}`));
|
|
59
|
+
}
|
|
60
|
+
const json = (await res.json());
|
|
61
|
+
const content = (json.candidates?.[0]?.content?.parts ?? [])
|
|
62
|
+
.map((p) => (typeof p.text === 'string' ? p.text : ''))
|
|
63
|
+
.join('');
|
|
64
|
+
return ok({
|
|
65
|
+
content,
|
|
66
|
+
model: json.modelVersion ?? model,
|
|
67
|
+
finishReason: json.candidates?.[0]?.finishReason,
|
|
68
|
+
usage: {
|
|
69
|
+
inputTokens: json.usageMetadata?.promptTokenCount,
|
|
70
|
+
outputTokens: json.usageMetadata?.candidatesTokenCount,
|
|
71
|
+
},
|
|
72
|
+
raw: json,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
return err(new AppErrorImpl(ERROR_CODES.IO_ERROR, `Failed to call Gemini: ${e.message}`, {
|
|
77
|
+
cause: e,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function collectSystem(messages) {
|
|
83
|
+
const parts = messages.filter((m) => m.role === AiMessageRole.System).map((m) => m.content);
|
|
84
|
+
return parts.length > 0 ? parts.join('\n\n') : undefined;
|
|
85
|
+
}
|
|
86
|
+
function collectContents(messages) {
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const m of messages) {
|
|
89
|
+
if (m.role === AiMessageRole.System)
|
|
90
|
+
continue;
|
|
91
|
+
out.push({
|
|
92
|
+
role: m.role === AiMessageRole.Assistant ? 'model' : 'user',
|
|
93
|
+
parts: [{ text: m.content }],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,13 @@ export * from './ai-request.js';
|
|
|
3
3
|
export * from './prompt/prompt-builder.js';
|
|
4
4
|
export * from './claude/claude-provider.js';
|
|
5
5
|
export * from './claude/claude-cli-adapter.js';
|
|
6
|
+
export * from './gemini/gemini-provider.js';
|
|
7
|
+
export * from './ollama/ollama-provider.js';
|
|
8
|
+
export * from './llamacpp/llama-cpp-provider.js';
|
|
9
|
+
export * from './provider-resolver.js';
|
|
10
|
+
export * from './pipeline/enhancement-pipeline.js';
|
|
11
|
+
export * from './llm-hints.js';
|
|
12
|
+
export * from './llm-recommendations.js';
|
|
13
|
+
export * from './delegate/delegate-edit-schema.js';
|
|
14
|
+
export * from './delegate/parse-delegate-edit.js';
|
|
6
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kCAAkC,CAAC;AACjD,cAAc,wBAAwB,CAAC;AACvC,cAAc,oCAAoC,CAAC;AACnD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,oCAAoC,CAAC;AACnD,cAAc,mCAAmC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,12 @@ export * from "./ai-request.js";
|
|
|
3
3
|
export * from "./prompt/prompt-builder.js";
|
|
4
4
|
export * from "./claude/claude-provider.js";
|
|
5
5
|
export * from "./claude/claude-cli-adapter.js";
|
|
6
|
+
export * from "./gemini/gemini-provider.js";
|
|
7
|
+
export * from "./ollama/ollama-provider.js";
|
|
8
|
+
export * from "./llamacpp/llama-cpp-provider.js";
|
|
9
|
+
export * from "./provider-resolver.js";
|
|
10
|
+
export * from "./pipeline/enhancement-pipeline.js";
|
|
11
|
+
export * from "./llm-hints.js";
|
|
12
|
+
export * from "./llm-recommendations.js";
|
|
13
|
+
export * from "./delegate/delegate-edit-schema.js";
|
|
14
|
+
export * from "./delegate/parse-delegate-edit.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
2
|
+
import { AbstractAiProvider } from '../ai-provider.js';
|
|
3
|
+
import { type IAiRequest, type IAiResponse } from '../ai-request.js';
|
|
4
|
+
/**
|
|
5
|
+
* In-process generative provider backed by `node-llama-cpp` (a Node
|
|
6
|
+
* binding for llama.cpp). No HTTP. No daemon. The model is loaded
|
|
7
|
+
* once into process memory and reused across requests.
|
|
8
|
+
*
|
|
9
|
+
* Configuration (env or `IAiProviderConfig`):
|
|
10
|
+
* - `LLAMACPP_MODEL_PATH` — absolute or repo-relative path to a
|
|
11
|
+
* local `.gguf` file. If unset, the
|
|
12
|
+
* provider is `isReady() === false`.
|
|
13
|
+
* - `LLAMACPP_CONTEXT_SIZE` — context window in tokens (default 8192).
|
|
14
|
+
* - `LLAMACPP_GPU` — `auto` (default) | `metal` | `cuda` | `off`.
|
|
15
|
+
*
|
|
16
|
+
* The first `send()` call pays the model-load cost (typically 1–10 s
|
|
17
|
+
* for a 3B Q4 model on Apple Silicon). Subsequent calls reuse
|
|
18
|
+
* the same `LlamaModel` + `LlamaContext`. A fresh `LlamaChatSession`
|
|
19
|
+
* is created per request so context isn't leaked between unrelated
|
|
20
|
+
* tasks.
|
|
21
|
+
*
|
|
22
|
+
* Tests can inject a fake generator via `_overrideForTests` to avoid
|
|
23
|
+
* pulling in the native binding and a 2 GB model file.
|
|
24
|
+
*/
|
|
25
|
+
export declare class LlamaCppProvider extends AbstractAiProvider {
|
|
26
|
+
readonly id = "llamacpp";
|
|
27
|
+
readonly name = "llama.cpp (in-process)";
|
|
28
|
+
/** Test hook — bypasses the native binding when set. */
|
|
29
|
+
static _overrideForTests: ((request: IAiRequest, modelPath: string) => Promise<IAiResponse>) | null;
|
|
30
|
+
/**
|
|
31
|
+
* Reads the module-level cache to expose the active model path for
|
|
32
|
+
* tools that need it (mostly the disposer). Returns null when no
|
|
33
|
+
* model has been loaded in this process.
|
|
34
|
+
*/
|
|
35
|
+
static activeModelPath(): string | null;
|
|
36
|
+
isReady(): boolean;
|
|
37
|
+
send(request: IAiRequest): Promise<Result<IAiResponse, AppError>>;
|
|
38
|
+
private ensureLoaded;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Release the loaded llama.cpp model + context so the process can
|
|
42
|
+
* exit cleanly.
|
|
43
|
+
*
|
|
44
|
+
* Without this, the libc++ destructor for the Metal device list
|
|
45
|
+
* aborts on `exit()` with `ggml_metal_device_free` because the
|
|
46
|
+
* device list isn't empty — same shape of teardown crash as the
|
|
47
|
+
* ONNX mutex issue, different native library. Disposing in the
|
|
48
|
+
* order session → context → model → llama lets the destructors
|
|
49
|
+
* run while the JS runtime is still healthy.
|
|
50
|
+
*
|
|
51
|
+
* Safe to call multiple times. Safe to call when no model was
|
|
52
|
+
* loaded. Errors during dispose are swallowed (the alternative is
|
|
53
|
+
* the abort we're trying to prevent).
|
|
54
|
+
*/
|
|
55
|
+
export declare function disposeLlamaCppRuntime(): Promise<boolean>;
|
|
56
|
+
//# sourceMappingURL=llama-cpp-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llama-cpp-provider.d.ts","sourceRoot":"","sources":["../../src/llamacpp/llama-cpp-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAkC,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAKrG;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAiB,SAAQ,kBAAkB;IACtD,QAAQ,CAAC,EAAE,cAAc;IACzB,QAAQ,CAAC,IAAI,4BAA4B;IAEzC,wDAAwD;IACxD,MAAM,CAAC,iBAAiB,EACpB,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,GAClE,IAAI,CAAQ;IAEhB;;;;OAIG;IACH,MAAM,CAAC,eAAe,IAAI,MAAM,GAAG,IAAI;IAIvC,OAAO,IAAI,OAAO;IAIZ,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;YAwKzD,YAAY;CAwB3B;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,OAAO,CAAC,CAc/D"}
|