@shrkcrft/ai 0.1.0-alpha.26 → 0.1.0-alpha.28
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/delegate/delegate-analysis-schema.d.ts +40 -0
- package/dist/delegate/delegate-analysis-schema.d.ts.map +1 -0
- package/dist/delegate/delegate-analysis-schema.js +44 -0
- package/dist/delegate/parse-delegate-analysis.d.ts +43 -0
- package/dist/delegate/parse-delegate-analysis.d.ts.map +1 -0
- package/dist/delegate/parse-delegate-analysis.js +124 -0
- package/dist/delegate/query-loop.d.ts +87 -0
- package/dist/delegate/query-loop.d.ts.map +1 -0
- package/dist/delegate/query-loop.js +162 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/package.json +3 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structured judgment an `analysis` delegate recipe asks a local model to
|
|
3
|
+
* emit. Unlike a `patch` recipe (which emits editable ops — see
|
|
4
|
+
* `delegate-edit-schema.ts`), an analysis recipe emits READ-ONLY findings: the
|
|
5
|
+
* model interprets / prioritises / explains a deterministic report the engine
|
|
6
|
+
* already computed. Each finding may cite `refs` (files / constructs / reason
|
|
7
|
+
* codes) drawn from the grounding facts; the inspector's grounding cross-check
|
|
8
|
+
* flags any ref that is NOT present in the ground truth as `unverified`, so the
|
|
9
|
+
* model adds judgment on top of facts it cannot fabricate.
|
|
10
|
+
*
|
|
11
|
+
* This layer (`@shrkcrft/ai`) holds only the wire shape + a structural parse —
|
|
12
|
+
* the grounding cross-check + report assembly live in `@shrkcrft/inspector`.
|
|
13
|
+
*/
|
|
14
|
+
/** One raw finding the model emits. `refs`/`id` are optional; `message` is not. */
|
|
15
|
+
export interface IDelegateRawFinding {
|
|
16
|
+
/** Stable-ish id the model may assign (informational; the engine re-keys). */
|
|
17
|
+
id?: string;
|
|
18
|
+
/** The judgment / observation. */
|
|
19
|
+
message: string;
|
|
20
|
+
/**
|
|
21
|
+
* Entities this finding references (file paths / construct ids / reason
|
|
22
|
+
* codes). MUST be drawn from the provided grounding facts — a ref absent from
|
|
23
|
+
* the ground truth is flagged `unverified` by the cross-check.
|
|
24
|
+
*/
|
|
25
|
+
refs?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** The full structured analysis returned by an analysis worker. */
|
|
28
|
+
export interface IDelegateRawAnalysis {
|
|
29
|
+
findings: readonly IDelegateRawFinding[];
|
|
30
|
+
/** Optional free-form note (informational; never treated as a grounded fact). */
|
|
31
|
+
note?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* JSON Schema handed to the provider as `responseFormat.schema` so a local model
|
|
35
|
+
* returns a parseable analysis. Deliberately closed (`additionalProperties:
|
|
36
|
+
* false`) — a weak model that invents fields fails the parse and is reprompted
|
|
37
|
+
* once rather than smuggling unstructured prose through.
|
|
38
|
+
*/
|
|
39
|
+
export declare const DELEGATE_ANALYSIS_JSON_SCHEMA: Record<string, unknown>;
|
|
40
|
+
//# sourceMappingURL=delegate-analysis-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delegate-analysis-schema.d.ts","sourceRoot":"","sources":["../../src/delegate/delegate-analysis-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAyBjE,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structured judgment an `analysis` delegate recipe asks a local model to
|
|
3
|
+
* emit. Unlike a `patch` recipe (which emits editable ops — see
|
|
4
|
+
* `delegate-edit-schema.ts`), an analysis recipe emits READ-ONLY findings: the
|
|
5
|
+
* model interprets / prioritises / explains a deterministic report the engine
|
|
6
|
+
* already computed. Each finding may cite `refs` (files / constructs / reason
|
|
7
|
+
* codes) drawn from the grounding facts; the inspector's grounding cross-check
|
|
8
|
+
* flags any ref that is NOT present in the ground truth as `unverified`, so the
|
|
9
|
+
* model adds judgment on top of facts it cannot fabricate.
|
|
10
|
+
*
|
|
11
|
+
* This layer (`@shrkcrft/ai`) holds only the wire shape + a structural parse —
|
|
12
|
+
* the grounding cross-check + report assembly live in `@shrkcrft/inspector`.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* JSON Schema handed to the provider as `responseFormat.schema` so a local model
|
|
16
|
+
* returns a parseable analysis. Deliberately closed (`additionalProperties:
|
|
17
|
+
* false`) — a weak model that invents fields fails the parse and is reprompted
|
|
18
|
+
* once rather than smuggling unstructured prose through.
|
|
19
|
+
*/
|
|
20
|
+
export const DELEGATE_ANALYSIS_JSON_SCHEMA = {
|
|
21
|
+
type: 'object',
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
required: ['findings'],
|
|
24
|
+
properties: {
|
|
25
|
+
note: { type: 'string' },
|
|
26
|
+
findings: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
required: ['message'],
|
|
32
|
+
properties: {
|
|
33
|
+
id: { type: 'string' },
|
|
34
|
+
message: { type: 'string', description: 'the judgment / observation' },
|
|
35
|
+
refs: {
|
|
36
|
+
type: 'array',
|
|
37
|
+
items: { type: 'string' },
|
|
38
|
+
description: 'files / constructs / reason-codes this finding references — MUST be drawn from the provided grounding facts',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + (lightly) validate the JSON an `analysis` delegate worker emits, and a
|
|
3
|
+
* one-shot generate→parse→reprompt-once helper. Mirrors `parse-delegate-edit.ts`:
|
|
4
|
+
* a PARSE failure reprompts once; a provider / TIMEOUT error surfaces immediately
|
|
5
|
+
* (the CLI 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 IDelegateRawAnalysis } from './delegate-analysis-schema.js';
|
|
11
|
+
/** Parse a worker's raw output into a structurally-validated `IDelegateRawAnalysis`. */
|
|
12
|
+
export declare function parseDelegateAnalysis(raw: string): Result<IDelegateRawAnalysis, AppError>;
|
|
13
|
+
export interface IDelegateAnalysisCallInput {
|
|
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
|
+
/** Build the reprompt messages after a PARSE failure. */
|
|
21
|
+
reprompt?: (badOutput: string, error: AppError) => readonly IAiMessage[];
|
|
22
|
+
}
|
|
23
|
+
export interface IDelegateAnalysisCallResult {
|
|
24
|
+
analysis: IDelegateRawAnalysis;
|
|
25
|
+
/** The raw model output that parsed (for telemetry / hand-back). */
|
|
26
|
+
raw: string;
|
|
27
|
+
model: string;
|
|
28
|
+
usage?: {
|
|
29
|
+
inputTokens?: number;
|
|
30
|
+
outputTokens?: number;
|
|
31
|
+
};
|
|
32
|
+
/** True when the first output failed to parse and the reprompt succeeded. */
|
|
33
|
+
retried: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generate an analysis, parsing the output. A provider / TIMEOUT error surfaces
|
|
37
|
+
* immediately; a PARSE failure reprompts ONCE (when a reprompt builder is
|
|
38
|
+
* supplied) before giving up.
|
|
39
|
+
*/
|
|
40
|
+
export declare function callDelegateAnalysisWithRetry(input: IDelegateAnalysisCallInput): Promise<Result<IDelegateAnalysisCallResult, AppError>>;
|
|
41
|
+
/** Convenience for callers building reprompt messages. */
|
|
42
|
+
export declare function delegateAnalysisRepromptMessage(badOutput: string, error: AppError): IAiMessage;
|
|
43
|
+
//# sourceMappingURL=parse-delegate-analysis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-delegate-analysis.d.ts","sourceRoot":"","sources":["../../src/delegate/parse-delegate-analysis.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,EAEL,KAAK,oBAAoB,EAE1B,MAAM,+BAA+B,CAAC;AAavC,wFAAwF;AACxF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAwCzF;AAED,MAAM,WAAW,0BAA0B;IACzC,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,yDAAyD;IACzD,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,SAAS,UAAU,EAAE,CAAC;CAC1E;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,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,6BAA6B,CACjD,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,MAAM,CAAC,2BAA2B,EAAE,QAAQ,CAAC,CAAC,CA0BxD;AAED,0DAA0D;AAC1D,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,UAAU,CAQ9F"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + (lightly) validate the JSON an `analysis` delegate worker emits, and a
|
|
3
|
+
* one-shot generate→parse→reprompt-once helper. Mirrors `parse-delegate-edit.ts`:
|
|
4
|
+
* a PARSE failure reprompts once; a provider / TIMEOUT error surfaces immediately
|
|
5
|
+
* (the CLI 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_ANALYSIS_JSON_SCHEMA, } from "./delegate-analysis-schema.js";
|
|
10
|
+
function invalid(message, cause) {
|
|
11
|
+
return new AppErrorImpl(ERROR_CODES.INVALID_INPUT, message, cause !== undefined ? { cause } : undefined);
|
|
12
|
+
}
|
|
13
|
+
/** Strip a leading/trailing markdown code fence weak local models often add. */
|
|
14
|
+
function stripFences(text) {
|
|
15
|
+
const fence = /^\s*```(?:json)?\s*\n([\s\S]*?)\n```\s*$/;
|
|
16
|
+
const m = text.match(fence);
|
|
17
|
+
return m ? m[1] : text;
|
|
18
|
+
}
|
|
19
|
+
/** Parse a worker's raw output into a structurally-validated `IDelegateRawAnalysis`. */
|
|
20
|
+
export function parseDelegateAnalysis(raw) {
|
|
21
|
+
const text = stripFences(raw).trim();
|
|
22
|
+
if (text.length === 0)
|
|
23
|
+
return err(invalid('delegate analysis is empty'));
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(text);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return err(invalid('delegate analysis is not valid JSON', e));
|
|
30
|
+
}
|
|
31
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
32
|
+
return err(invalid('delegate analysis must be a JSON object'));
|
|
33
|
+
}
|
|
34
|
+
const obj = parsed;
|
|
35
|
+
const findingsRaw = obj['findings'];
|
|
36
|
+
if (!Array.isArray(findingsRaw))
|
|
37
|
+
return err(invalid('delegate analysis "findings" must be an array'));
|
|
38
|
+
const findings = [];
|
|
39
|
+
for (let i = 0; i < findingsRaw.length; i += 1) {
|
|
40
|
+
const f = findingsRaw[i];
|
|
41
|
+
if (!f || typeof f !== 'object' || Array.isArray(f)) {
|
|
42
|
+
return err(invalid(`findings[${i}] must be an object`));
|
|
43
|
+
}
|
|
44
|
+
const ff = f;
|
|
45
|
+
const message = ff['message'];
|
|
46
|
+
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
47
|
+
return err(invalid(`findings[${i}].message must be a non-empty string`));
|
|
48
|
+
}
|
|
49
|
+
const finding = { message };
|
|
50
|
+
if (typeof ff['id'] === 'string')
|
|
51
|
+
finding.id = ff['id'];
|
|
52
|
+
const refs = ff['refs'];
|
|
53
|
+
if (refs !== undefined) {
|
|
54
|
+
if (!Array.isArray(refs) || refs.some((r) => typeof r !== 'string')) {
|
|
55
|
+
return err(invalid(`findings[${i}].refs must be an array of strings`));
|
|
56
|
+
}
|
|
57
|
+
finding.refs = refs;
|
|
58
|
+
}
|
|
59
|
+
findings.push(finding);
|
|
60
|
+
}
|
|
61
|
+
const analysis = { findings };
|
|
62
|
+
if (typeof obj['note'] === 'string')
|
|
63
|
+
analysis.note = obj['note'];
|
|
64
|
+
return ok(analysis);
|
|
65
|
+
}
|
|
66
|
+
async function sendOnce(input, messages) {
|
|
67
|
+
if (input.model)
|
|
68
|
+
input.provider.configure({ model: input.model });
|
|
69
|
+
const res = await input.provider.send({
|
|
70
|
+
messages,
|
|
71
|
+
...(input.model ? { model: input.model } : {}),
|
|
72
|
+
...(input.maxTokens ? { maxTokens: input.maxTokens } : {}),
|
|
73
|
+
...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
|
|
74
|
+
responseFormat: { type: 'json_schema', schema: DELEGATE_ANALYSIS_JSON_SCHEMA, schemaName: 'DelegateAnalysis' },
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
return res;
|
|
78
|
+
return ok({ content: res.value.content, model: res.value.model, ...(res.value.usage ? { usage: res.value.usage } : {}) });
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Generate an analysis, parsing the output. A provider / TIMEOUT error surfaces
|
|
82
|
+
* immediately; a PARSE failure reprompts ONCE (when a reprompt builder is
|
|
83
|
+
* supplied) before giving up.
|
|
84
|
+
*/
|
|
85
|
+
export async function callDelegateAnalysisWithRetry(input) {
|
|
86
|
+
const first = await sendOnce(input, input.messages);
|
|
87
|
+
if (!first.ok)
|
|
88
|
+
return first;
|
|
89
|
+
const parsed = parseDelegateAnalysis(first.value.content);
|
|
90
|
+
if (parsed.ok) {
|
|
91
|
+
return ok({
|
|
92
|
+
analysis: parsed.value,
|
|
93
|
+
raw: first.value.content,
|
|
94
|
+
model: first.value.model,
|
|
95
|
+
...(first.value.usage ? { usage: first.value.usage } : {}),
|
|
96
|
+
retried: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!input.reprompt)
|
|
100
|
+
return err(parsed.error);
|
|
101
|
+
const retryMessages = input.reprompt(first.value.content, parsed.error);
|
|
102
|
+
const second = await sendOnce(input, retryMessages);
|
|
103
|
+
if (!second.ok)
|
|
104
|
+
return second;
|
|
105
|
+
const reparsed = parseDelegateAnalysis(second.value.content);
|
|
106
|
+
if (!reparsed.ok)
|
|
107
|
+
return err(reparsed.error);
|
|
108
|
+
return ok({
|
|
109
|
+
analysis: reparsed.value,
|
|
110
|
+
raw: second.value.content,
|
|
111
|
+
model: second.value.model,
|
|
112
|
+
...(second.value.usage ? { usage: second.value.usage } : {}),
|
|
113
|
+
retried: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Convenience for callers building reprompt messages. */
|
|
117
|
+
export function delegateAnalysisRepromptMessage(badOutput, error) {
|
|
118
|
+
return {
|
|
119
|
+
role: AiMessageRole.User,
|
|
120
|
+
content: `Your previous reply could not be parsed: ${error.message}\n` +
|
|
121
|
+
`It must be a single JSON object matching the schema — no prose, no markdown fences.\n` +
|
|
122
|
+
`Previous reply was:\n${badOutput.slice(0, 2000)}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded, read-only QUERY LOOP for `analysis` delegate recipes (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Generalises the one-shot `--ai-plan` context-expansion into an iterated loop:
|
|
5
|
+
* each round the model may REQUEST a few read-only engine facts (as JSON, via the
|
|
6
|
+
* schema below — NOT native provider tool-calling, which is brittle with weak
|
|
7
|
+
* local models and would need surgery on the neutral message model); the engine
|
|
8
|
+
* runs only the allow-listed queries, feeds the results back, and repeats until
|
|
9
|
+
* the model is `done`, the round cap is hit, or the wall-clock budget is spent.
|
|
10
|
+
* Then the model produces its final answer with the caller's response format.
|
|
11
|
+
*
|
|
12
|
+
* Layer-clean: the query EXECUTION is injected (`executeQuery`) — this AI-layer
|
|
13
|
+
* runtime never imports the engine. There is deliberately no write/apply/gen/sign
|
|
14
|
+
* query: this is a fact-fetch loop, not an agent.
|
|
15
|
+
*/
|
|
16
|
+
import { type AppError, type Result } from '@shrkcrft/core';
|
|
17
|
+
import type { IAiProvider } from '../ai-provider.js';
|
|
18
|
+
import { type IAiMessage, type IAiResponseFormat } from '../ai-request.js';
|
|
19
|
+
/** One read-only query the model may request. */
|
|
20
|
+
export interface IQueryDescriptor {
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The result of running one read-only query. */
|
|
25
|
+
export interface IQueryResult {
|
|
26
|
+
/** Compact text the model sees. */
|
|
27
|
+
content: string;
|
|
28
|
+
/** Entities this query surfaced — merged into the analysis ground truth. */
|
|
29
|
+
entities: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
/** Runs one allow-listed read-only query. Injected by the cli (never in this layer). */
|
|
32
|
+
export type QueryExecutor = (name: string, args: Record<string, unknown>) => Promise<IQueryResult>;
|
|
33
|
+
export interface IQueryLogEntry {
|
|
34
|
+
name: string;
|
|
35
|
+
args: Record<string, unknown>;
|
|
36
|
+
/** false = refused (not allow-listed) or the executor threw. */
|
|
37
|
+
ok: boolean;
|
|
38
|
+
result: string;
|
|
39
|
+
}
|
|
40
|
+
export interface IRunQueryLoopInput {
|
|
41
|
+
provider: IAiProvider;
|
|
42
|
+
messages: readonly IAiMessage[];
|
|
43
|
+
model?: string;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
maxTokens?: number;
|
|
46
|
+
/** The recipe's read-only query allow-list — the only names the model may call. */
|
|
47
|
+
allowedQueries: readonly string[];
|
|
48
|
+
/** Max planning rounds. Clamped to `[0, 4]`; `0` ⇒ straight to the final answer. */
|
|
49
|
+
maxQueryRounds: number;
|
|
50
|
+
/** Wall-clock budget for the whole loop (best-effort; checked between rounds). */
|
|
51
|
+
budgetMs?: number;
|
|
52
|
+
/** Optional human descriptions of each query, shown to the model. */
|
|
53
|
+
catalog?: readonly IQueryDescriptor[];
|
|
54
|
+
executeQuery: QueryExecutor;
|
|
55
|
+
/** Instruction appended before the final answer turn. */
|
|
56
|
+
finalInstruction: string;
|
|
57
|
+
/** Response format for the FINAL answer (e.g. the analysis findings schema). */
|
|
58
|
+
finalResponseFormat?: IAiResponseFormat;
|
|
59
|
+
/** Cap on queries executed per round (default 4). */
|
|
60
|
+
maxQueriesPerRound?: number;
|
|
61
|
+
/** Injectable clock (default `Date.now`) so budget behaviour is testable. */
|
|
62
|
+
now?: () => number;
|
|
63
|
+
}
|
|
64
|
+
export interface IRunQueryLoopResult {
|
|
65
|
+
content: string;
|
|
66
|
+
model: string;
|
|
67
|
+
usage?: {
|
|
68
|
+
inputTokens?: number;
|
|
69
|
+
outputTokens?: number;
|
|
70
|
+
};
|
|
71
|
+
/** How many planning rounds actually ran. */
|
|
72
|
+
roundsRun: number;
|
|
73
|
+
/** Audit log of every query requested (executed or refused). */
|
|
74
|
+
queries: readonly IQueryLogEntry[];
|
|
75
|
+
/** Union of entities surfaced by executed queries. */
|
|
76
|
+
gatheredEntities: readonly string[];
|
|
77
|
+
/** True when no query was ever run (rounds 0, or the model never queried). */
|
|
78
|
+
degraded: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run the bounded query loop, then the final answer turn. A provider error at any
|
|
82
|
+
* point surfaces as `err`. Query fences (allow-list, round cap, budget, per-round
|
|
83
|
+
* cap) are enforced deterministically here — the model can only influence WHICH
|
|
84
|
+
* allow-listed facts are fetched, never whether the fence holds.
|
|
85
|
+
*/
|
|
86
|
+
export declare function runBoundedQueryLoop(input: IRunQueryLoopInput): Promise<Result<IRunQueryLoopResult, AppError>>;
|
|
87
|
+
//# sourceMappingURL=query-loop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-loop.d.ts","sourceRoot":"","sources":["../../src/delegate/query-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAM,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAiB,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1F,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wFAAwF;AACxF,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAEnG,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,gEAAgE;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oFAAoF;IACpF,cAAc,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,YAAY,EAAE,aAAa,CAAC;IAC5B,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,iBAAiB,CAAC;IACxC,qDAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,sDAAsD;IACtD,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAwED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC,CAqEhD"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded, read-only QUERY LOOP for `analysis` delegate recipes (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Generalises the one-shot `--ai-plan` context-expansion into an iterated loop:
|
|
5
|
+
* each round the model may REQUEST a few read-only engine facts (as JSON, via the
|
|
6
|
+
* schema below — NOT native provider tool-calling, which is brittle with weak
|
|
7
|
+
* local models and would need surgery on the neutral message model); the engine
|
|
8
|
+
* runs only the allow-listed queries, feeds the results back, and repeats until
|
|
9
|
+
* the model is `done`, the round cap is hit, or the wall-clock budget is spent.
|
|
10
|
+
* Then the model produces its final answer with the caller's response format.
|
|
11
|
+
*
|
|
12
|
+
* Layer-clean: the query EXECUTION is injected (`executeQuery`) — this AI-layer
|
|
13
|
+
* runtime never imports the engine. There is deliberately no write/apply/gen/sign
|
|
14
|
+
* query: this is a fact-fetch loop, not an agent.
|
|
15
|
+
*/
|
|
16
|
+
import { ok } from '@shrkcrft/core';
|
|
17
|
+
import { AiMessageRole } from "../ai-request.js";
|
|
18
|
+
/** JSON schema for the per-round query request, with the allow-list as the enum. */
|
|
19
|
+
function queryRequestSchema(allowed) {
|
|
20
|
+
return {
|
|
21
|
+
type: 'object',
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
required: ['done'],
|
|
24
|
+
properties: {
|
|
25
|
+
done: { type: 'boolean', description: 'true when you have enough facts to answer' },
|
|
26
|
+
queries: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
required: ['name'],
|
|
32
|
+
properties: {
|
|
33
|
+
name: { type: 'string', enum: [...allowed] },
|
|
34
|
+
args: { type: 'object', additionalProperties: true },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function stripFences(text) {
|
|
42
|
+
const m = text.match(/^\s*```(?:json)?\s*\n([\s\S]*?)\n```\s*$/);
|
|
43
|
+
return m ? m[1] : text;
|
|
44
|
+
}
|
|
45
|
+
/** Best-effort parse of a query-request turn; returns null if unusable. */
|
|
46
|
+
function parseQueryRequest(raw) {
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(stripFences(raw).trim());
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
55
|
+
return null;
|
|
56
|
+
const obj = parsed;
|
|
57
|
+
const done = obj['done'] === true;
|
|
58
|
+
const out = { done, queries: [] };
|
|
59
|
+
if (Array.isArray(obj['queries'])) {
|
|
60
|
+
for (const q of obj['queries']) {
|
|
61
|
+
if (!q || typeof q !== 'object')
|
|
62
|
+
continue;
|
|
63
|
+
const name = q['name'];
|
|
64
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
const args = q['args'];
|
|
67
|
+
out.queries.push({ name, args: args && typeof args === 'object' && !Array.isArray(args) ? args : {} });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function queryInstruction(allowed, catalog, rounds) {
|
|
73
|
+
const byName = new Map((catalog ?? []).map((c) => [c.name, c.description ?? '']));
|
|
74
|
+
const lines = allowed.map((n) => ` - ${n}${byName.get(n) ? `: ${byName.get(n)}` : ''}`);
|
|
75
|
+
return [
|
|
76
|
+
`You may first pull READ-ONLY facts (up to ${rounds} round(s)) before answering.`,
|
|
77
|
+
'Available queries:',
|
|
78
|
+
...lines,
|
|
79
|
+
'Respond with JSON: {"done": <bool>, "queries": [{"name": "<query>", "args": {...}}]}.',
|
|
80
|
+
'Request only the facts you need. Set "done": true (and no queries) when ready to answer.',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Run the bounded query loop, then the final answer turn. A provider error at any
|
|
85
|
+
* point surfaces as `err`. Query fences (allow-list, round cap, budget, per-round
|
|
86
|
+
* cap) are enforced deterministically here — the model can only influence WHICH
|
|
87
|
+
* allow-listed facts are fetched, never whether the fence holds.
|
|
88
|
+
*/
|
|
89
|
+
export async function runBoundedQueryLoop(input) {
|
|
90
|
+
const now = input.now ?? Date.now;
|
|
91
|
+
const rounds = Math.max(0, Math.min(4, Math.floor(input.maxQueryRounds)));
|
|
92
|
+
const allowed = new Set(input.allowedQueries);
|
|
93
|
+
const perRound = Math.max(1, input.maxQueriesPerRound ?? 4);
|
|
94
|
+
const conversation = [...input.messages];
|
|
95
|
+
const queries = [];
|
|
96
|
+
const gathered = new Set();
|
|
97
|
+
const startedAt = now();
|
|
98
|
+
let roundsRun = 0;
|
|
99
|
+
if (rounds > 0 && allowed.size > 0) {
|
|
100
|
+
conversation.push({ role: AiMessageRole.User, content: queryInstruction(input.allowedQueries, input.catalog, rounds) });
|
|
101
|
+
const requestSchema = queryRequestSchema(input.allowedQueries);
|
|
102
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
103
|
+
if (input.budgetMs && now() - startedAt > input.budgetMs)
|
|
104
|
+
break;
|
|
105
|
+
roundsRun += 1;
|
|
106
|
+
const res = await input.provider.send({
|
|
107
|
+
messages: conversation,
|
|
108
|
+
...(input.model ? { model: input.model } : {}),
|
|
109
|
+
...(input.maxTokens ? { maxTokens: input.maxTokens } : {}),
|
|
110
|
+
...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
|
|
111
|
+
responseFormat: { type: 'json_schema', schema: requestSchema, schemaName: 'QueryRequest' },
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
return res;
|
|
115
|
+
const parsed = parseQueryRequest(res.value.content);
|
|
116
|
+
if (!parsed || parsed.done || parsed.queries.length === 0)
|
|
117
|
+
break;
|
|
118
|
+
const resultLines = [];
|
|
119
|
+
for (const q of parsed.queries.slice(0, perRound)) {
|
|
120
|
+
if (!allowed.has(q.name)) {
|
|
121
|
+
queries.push({ name: q.name, args: q.args, ok: false, result: 'refused: not in the allowed query list' });
|
|
122
|
+
resultLines.push(`- ${q.name}: REFUSED (not permitted)`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const out = await input.executeQuery(q.name, q.args);
|
|
127
|
+
queries.push({ name: q.name, args: q.args, ok: true, result: out.content });
|
|
128
|
+
for (const e of out.entities)
|
|
129
|
+
gathered.add(e);
|
|
130
|
+
resultLines.push(`- ${q.name}(${JSON.stringify(q.args)}):\n${out.content}`);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
queries.push({ name: q.name, args: q.args, ok: false, result: `error: ${e.message}` });
|
|
134
|
+
resultLines.push(`- ${q.name}: ERROR ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
conversation.push({
|
|
138
|
+
role: AiMessageRole.User,
|
|
139
|
+
content: `Query results:\n${resultLines.join('\n')}\n\nRequest more facts (done:false) or set done:true to answer now.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
conversation.push({ role: AiMessageRole.User, content: input.finalInstruction });
|
|
144
|
+
const finalRes = await input.provider.send({
|
|
145
|
+
messages: conversation,
|
|
146
|
+
...(input.model ? { model: input.model } : {}),
|
|
147
|
+
...(input.maxTokens ? { maxTokens: input.maxTokens } : {}),
|
|
148
|
+
...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
|
|
149
|
+
...(input.finalResponseFormat ? { responseFormat: input.finalResponseFormat } : {}),
|
|
150
|
+
});
|
|
151
|
+
if (!finalRes.ok)
|
|
152
|
+
return finalRes;
|
|
153
|
+
return ok({
|
|
154
|
+
content: finalRes.value.content,
|
|
155
|
+
model: finalRes.value.model,
|
|
156
|
+
...(finalRes.value.usage ? { usage: finalRes.value.usage } : {}),
|
|
157
|
+
roundsRun,
|
|
158
|
+
queries,
|
|
159
|
+
gatheredEntities: [...gathered],
|
|
160
|
+
degraded: queries.length === 0,
|
|
161
|
+
});
|
|
162
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,7 @@ export * from './llm-hints.js';
|
|
|
11
11
|
export * from './llm-recommendations.js';
|
|
12
12
|
export * from './delegate/delegate-edit-schema.js';
|
|
13
13
|
export * from './delegate/parse-delegate-edit.js';
|
|
14
|
+
export * from './delegate/delegate-analysis-schema.js';
|
|
15
|
+
export * from './delegate/parse-delegate-analysis.js';
|
|
16
|
+
export * from './delegate/query-loop.js';
|
|
14
17
|
//# 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,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"}
|
|
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,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;AAClD,cAAc,wCAAwC,CAAC;AACvD,cAAc,uCAAuC,CAAC;AACtD,cAAc,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -11,3 +11,6 @@ export * from "./llm-hints.js";
|
|
|
11
11
|
export * from "./llm-recommendations.js";
|
|
12
12
|
export * from "./delegate/delegate-edit-schema.js";
|
|
13
13
|
export * from "./delegate/parse-delegate-edit.js";
|
|
14
|
+
export * from "./delegate/delegate-analysis-schema.js";
|
|
15
|
+
export * from "./delegate/parse-delegate-analysis.js";
|
|
16
|
+
export * from "./delegate/query-loop.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/ai",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.28",
|
|
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.
|
|
47
|
-
"@shrkcrft/context": "^0.1.0-alpha.
|
|
46
|
+
"@shrkcrft/core": "^0.1.0-alpha.28",
|
|
47
|
+
"@shrkcrft/context": "^0.1.0-alpha.28",
|
|
48
48
|
"node-llama-cpp": "^3.16.0"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|