@shrkcrft/ai 0.1.0-alpha.16 → 0.1.0-alpha.18

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.
@@ -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
+ }
package/dist/index.d.ts CHANGED
@@ -10,4 +10,6 @@ export * from './provider-resolver.js';
10
10
  export * from './pipeline/enhancement-pipeline.js';
11
11
  export * from './llm-hints.js';
12
12
  export * from './llm-recommendations.js';
13
+ export * from './delegate/delegate-edit-schema.js';
14
+ export * from './delegate/parse-delegate-edit.js';
13
15
  //# sourceMappingURL=index.d.ts.map
@@ -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;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"}
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
@@ -10,3 +10,5 @@ export * from "./provider-resolver.js";
10
10
  export * from "./pipeline/enhancement-pipeline.js";
11
11
  export * from "./llm-hints.js";
12
12
  export * from "./llm-recommendations.js";
13
+ export * from "./delegate/delegate-edit-schema.js";
14
+ export * from "./delegate/parse-delegate-edit.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/ai",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.18",
4
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",
@@ -43,8 +43,8 @@
43
43
  "typecheck": "tsc --noEmit -p tsconfig.json"
44
44
  },
45
45
  "dependencies": {
46
- "@shrkcrft/core": "^0.1.0-alpha.16",
47
- "@shrkcrft/context": "^0.1.0-alpha.16",
46
+ "@shrkcrft/core": "^0.1.0-alpha.18",
47
+ "@shrkcrft/context": "^0.1.0-alpha.18",
48
48
  "node-llama-cpp": "^3.16.0"
49
49
  },
50
50
  "publishConfig": {