@alint-js/core 0.0.19 → 0.0.20
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/README.md +40 -25
- package/dist/index.mjs +1 -1
- package/dist/structured-output.d.mts +48 -0
- package/dist/structured-output.mjs +186 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -4,39 +4,21 @@ The public SDK and run engine for `alint`.
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
This package provides the core APIs used by
|
|
7
|
+
This package provides the core SDK and run engine APIs used by plugins, rules, language processors, embedding tools, and agent adapters:
|
|
8
8
|
|
|
9
|
-
- `
|
|
9
|
+
- `definePlugin` and `defineRule`
|
|
10
|
+
- `runAlint`
|
|
10
11
|
- rule registry and flat config normalization
|
|
11
12
|
- source runtime helpers
|
|
12
13
|
- built-in JavaScript source extraction
|
|
13
14
|
- model resolution by size and capability
|
|
14
15
|
- diagnostics and progress payload types
|
|
15
|
-
- `runAlint`
|
|
16
16
|
- framework-neutral agent contracts under `@alint-js/core/agent`
|
|
17
|
+
- tool-call structured output under `@alint-js/core/structured-output`
|
|
18
|
+
- config DSL and types for advanced SDK consumers
|
|
17
19
|
|
|
18
20
|
## How to use
|
|
19
21
|
|
|
20
|
-
Define an `alint` config:
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import examplePlugin from '@alint-js/plugin-example'
|
|
24
|
-
|
|
25
|
-
import { defineConfig } from '@alint-js/core'
|
|
26
|
-
|
|
27
|
-
export default defineConfig([
|
|
28
|
-
{
|
|
29
|
-
files: ['**/*.{js,ts,jsx,tsx}'],
|
|
30
|
-
plugins: {
|
|
31
|
-
example: examplePlugin,
|
|
32
|
-
},
|
|
33
|
-
rules: {
|
|
34
|
-
'example/no-redundant-jsdoc': 'warn',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
])
|
|
38
|
-
```
|
|
39
|
-
|
|
40
22
|
Write a rule:
|
|
41
23
|
|
|
42
24
|
```ts
|
|
@@ -65,6 +47,39 @@ import { requireAgent } from '@alint-js/core/agent'
|
|
|
65
47
|
const agent = requireAgent(ctx)
|
|
66
48
|
```
|
|
67
49
|
|
|
50
|
+
Ask a model for one validated, typed result with `@alint-js/core/structured-output`. It forces
|
|
51
|
+
the model to call a single reporting tool whose arguments match a valibot schema, validates
|
|
52
|
+
them, and retries with the validation error fed back to the model:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { generateStructured } from '@alint-js/core/structured-output'
|
|
56
|
+
import { array, description, object, pipe } from 'valibot'
|
|
57
|
+
|
|
58
|
+
const responseSchema = pipe(
|
|
59
|
+
object({ findings: array(findingSchema) }),
|
|
60
|
+
description('Report findings for this file.'),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const { findings } = await generateStructured({
|
|
64
|
+
createMessages: retryFeedback => [
|
|
65
|
+
{ content: prompt, role: 'system' },
|
|
66
|
+
...(retryFeedback ? [{ content: retryFeedback, role: 'user' as const }] : []),
|
|
67
|
+
{ content: numberedSource, role: 'user' },
|
|
68
|
+
],
|
|
69
|
+
logger: ctx.logger,
|
|
70
|
+
metering: ctx.metering,
|
|
71
|
+
model: await ctx.model(),
|
|
72
|
+
operation: 'my-rule-judge',
|
|
73
|
+
schema: responseSchema,
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The reporting tool is named `reportFindings` by default (`toolName` overrides it) and its
|
|
78
|
+
description defaults to the schema's valibot `description(...)`. `toolParametersFromSchema`,
|
|
79
|
+
`formatSourceWithLineNumbers`, and `formatOutputLanguageInstruction` are exported for callers
|
|
80
|
+
that build their own tools or prompts. Use `ctx.agent` instead when the model needs to
|
|
81
|
+
explore with tools before answering, because a forced tool call is a single shot, not a loop.
|
|
82
|
+
|
|
68
83
|
## When to use
|
|
69
84
|
|
|
70
85
|
- You are writing an `alint` plugin or rule package.
|
|
@@ -74,5 +89,5 @@ const agent = requireAgent(ctx)
|
|
|
74
89
|
|
|
75
90
|
## When not to use
|
|
76
91
|
|
|
77
|
-
- Use `@alint-js/cli` for command-line usage.
|
|
78
|
-
- Use `@alint-js/config` for setup TOML and config-file
|
|
92
|
+
- Use `@alint-js/cli` for command-line usage and ordinary `alint.config.*` files.
|
|
93
|
+
- Use `@alint-js/config` for setup TOML, config loading, and config-file tooling only.
|
package/dist/index.mjs
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { W as ResolvedModel, g as RuleContext } from "./types-DQfoxG8J.mjs";
|
|
2
|
+
import { GenericSchema, InferOutput } from "valibot";
|
|
3
|
+
import { JsonSchema } from "@valibot/to-json-schema";
|
|
4
|
+
import { Message } from "@xsai/shared-chat";
|
|
5
|
+
|
|
6
|
+
//#region src/structuredOutput/index.d.ts
|
|
7
|
+
interface GenerateStructuredOptions<Schema extends GenericSchema> {
|
|
8
|
+
/**
|
|
9
|
+
* Builds the chat messages for each attempt. On retries, `retryFeedback`
|
|
10
|
+
* carries a ready-to-send validation-failure message; insert it wherever it
|
|
11
|
+
* fits the conversation, usually right before the final user message.
|
|
12
|
+
*/
|
|
13
|
+
createMessages: (retryFeedback?: string) => Message[];
|
|
14
|
+
logger?: RuleContext['logger'];
|
|
15
|
+
/** Maximum number of attempts when validation fails. Defaults to 3. */
|
|
16
|
+
maxAttempts?: number;
|
|
17
|
+
metering?: RuleContext['metering'];
|
|
18
|
+
model: ResolvedModel;
|
|
19
|
+
/** Label recorded in metering metadata and debug logs, e.g. `go-responsibility-boundary-judge`. */
|
|
20
|
+
operation: string;
|
|
21
|
+
/** Milliseconds to wait before the given (1-based) attempt is retried. */
|
|
22
|
+
retryDelay?: (attempt: number) => number;
|
|
23
|
+
schema: Schema;
|
|
24
|
+
temperature?: number;
|
|
25
|
+
/** Shown to the model as the tool description. Defaults to the schema's valibot description. */
|
|
26
|
+
toolDescription?: string;
|
|
27
|
+
toolName?: string;
|
|
28
|
+
}
|
|
29
|
+
declare class InvalidStructuredOutputError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
/** Standard instruction line for localized judge findings; undefined when no language is configured. */
|
|
33
|
+
declare function formatOutputLanguageInstruction(outputLanguage: string | undefined): string | undefined;
|
|
34
|
+
/** Prefixes each line with its 1-based number so schemas can ask for "left-column line numbers". */
|
|
35
|
+
declare function formatSourceWithLineNumbers(source: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Forces the model to call a single reporting tool and returns the validated
|
|
38
|
+
* tool arguments, so a tool call doubles as structured output. Unlike
|
|
39
|
+
* `response_format`-based structured output (xsai's `generateObject`), a
|
|
40
|
+
* forced tool call works on any provider with function calling, and invalid
|
|
41
|
+
* payloads are retried with validation feedback instead of failing outright.
|
|
42
|
+
*/
|
|
43
|
+
declare function generateStructured<Schema extends GenericSchema>(options: GenerateStructuredOptions<Schema>): Promise<InferOutput<Schema>>;
|
|
44
|
+
declare function normalizeToolJsonSchema(schema: JsonSchema): JsonSchema;
|
|
45
|
+
/** Converts a valibot schema into provider-compliant strict tool parameters. */
|
|
46
|
+
declare function toolParametersFromSchema(schema: GenericSchema): JsonSchema;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { GenerateStructuredOptions, InvalidStructuredOutputError, formatOutputLanguageInstruction, formatSourceWithLineNumbers, generateStructured, normalizeToolJsonSchema, toolParametersFromSchema };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { errorMessageFrom } from "@moeru/std/error";
|
|
2
|
+
import { getDescription, parse } from "valibot";
|
|
3
|
+
import { sleep } from "@moeru/std/sleep";
|
|
4
|
+
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
5
|
+
import { generateText } from "@xsai/generate-text";
|
|
6
|
+
import { rawTool } from "@xsai/tool";
|
|
7
|
+
//#region src/structuredOutput/index.ts
|
|
8
|
+
const defaultMaxAttempts = 3;
|
|
9
|
+
const defaultToolName = "reportFindings";
|
|
10
|
+
var InvalidStructuredOutputError = class extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "InvalidStructuredOutputError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Standard instruction line for localized judge findings; undefined when no language is configured. */
|
|
17
|
+
function formatOutputLanguageInstruction(outputLanguage) {
|
|
18
|
+
return outputLanguage ? `Write all human-readable finding messages and suggestions in this language: ${outputLanguage}.` : void 0;
|
|
19
|
+
}
|
|
20
|
+
/** Prefixes each line with its 1-based number so schemas can ask for "left-column line numbers". */
|
|
21
|
+
function formatSourceWithLineNumbers(source) {
|
|
22
|
+
return source.split("\n").map((line, index) => `${index + 1} | ${line}`).join("\n");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Forces the model to call a single reporting tool and returns the validated
|
|
26
|
+
* tool arguments, so a tool call doubles as structured output. Unlike
|
|
27
|
+
* `response_format`-based structured output (xsai's `generateObject`), a
|
|
28
|
+
* forced tool call works on any provider with function calling, and invalid
|
|
29
|
+
* payloads are retried with validation feedback instead of failing outright.
|
|
30
|
+
*/
|
|
31
|
+
async function generateStructured(options) {
|
|
32
|
+
const maxAttempts = options.maxAttempts ?? defaultMaxAttempts;
|
|
33
|
+
const retryDelay = options.retryDelay ?? exponentialRetryDelay;
|
|
34
|
+
const toolName = options.toolName ?? defaultToolName;
|
|
35
|
+
const tool = rawTool({
|
|
36
|
+
description: options.toolDescription ?? getDescription(options.schema),
|
|
37
|
+
execute: (input) => asRecord(input) ?? {},
|
|
38
|
+
name: toolName,
|
|
39
|
+
parameters: toolParametersFromSchema(options.schema),
|
|
40
|
+
strict: true
|
|
41
|
+
});
|
|
42
|
+
let previousError;
|
|
43
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await generateText({
|
|
47
|
+
baseURL: options.model.provider.endpoint,
|
|
48
|
+
headers: options.model.provider.headers,
|
|
49
|
+
messages: options.createMessages(previousError ? retryFeedbackFrom(toolName, previousError) : void 0),
|
|
50
|
+
model: options.model.id,
|
|
51
|
+
parallelToolCalls: false,
|
|
52
|
+
temperature: options.temperature ?? 0,
|
|
53
|
+
toolChoice: {
|
|
54
|
+
function: { name: toolName },
|
|
55
|
+
type: "function"
|
|
56
|
+
},
|
|
57
|
+
tools: [tool]
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
previousError = `Tool call failed before validation: ${errorMessageFrom(error) ?? String(error)}`;
|
|
61
|
+
options.logger?.debug(`${options.operation} attempt ${attempt} failed while calling the model: ${previousError}`);
|
|
62
|
+
if (!isRetriableCallError(error) || attempt === maxAttempts) throw error;
|
|
63
|
+
await sleep(retryDelay(attempt));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
recordAttemptUsage(options, response);
|
|
67
|
+
const result = parseStructuredResponse(options.schema, toolName, response);
|
|
68
|
+
if (result.ok) return result.value;
|
|
69
|
+
previousError = result.error;
|
|
70
|
+
options.logger?.debug(`${options.operation} attempt ${attempt} returned an invalid structured result: ${previousError}`);
|
|
71
|
+
if (!result.retriable || attempt === maxAttempts) throw new InvalidStructuredOutputError(`Invalid structured model response: ${previousError}`);
|
|
72
|
+
await sleep(retryDelay(attempt));
|
|
73
|
+
}
|
|
74
|
+
throw new InvalidStructuredOutputError("Model did not return a valid structured result");
|
|
75
|
+
}
|
|
76
|
+
function normalizeToolJsonSchema(schema) {
|
|
77
|
+
const normalized = normalizeJsonSchemaDefinition(schema);
|
|
78
|
+
return typeof normalized === "boolean" ? {} : normalized;
|
|
79
|
+
}
|
|
80
|
+
/** Converts a valibot schema into provider-compliant strict tool parameters. */
|
|
81
|
+
function toolParametersFromSchema(schema) {
|
|
82
|
+
return normalizeToolJsonSchema(toJsonSchema(schema));
|
|
83
|
+
}
|
|
84
|
+
function asRecord(value) {
|
|
85
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
86
|
+
}
|
|
87
|
+
function exponentialRetryDelay(attempt) {
|
|
88
|
+
return 500 * 2 ** (attempt - 1);
|
|
89
|
+
}
|
|
90
|
+
function isRetriableCallError(error) {
|
|
91
|
+
if (!(error instanceof Error)) return true;
|
|
92
|
+
return error.name === "InvalidToolCallError" || error.name === "InvalidToolInputError" || error.name === "ToolExecutionError";
|
|
93
|
+
}
|
|
94
|
+
function normalizeJsonSchemaDefinition(schema) {
|
|
95
|
+
if (typeof schema === "boolean") return schema;
|
|
96
|
+
const normalized = { ...schema };
|
|
97
|
+
delete normalized.$schema;
|
|
98
|
+
if (normalized.type === "object") normalized.additionalProperties = false;
|
|
99
|
+
if (normalized.properties) {
|
|
100
|
+
normalized.properties = Object.fromEntries(Object.entries(normalized.properties).map(([key, propertySchema]) => [key, normalizeJsonSchemaDefinition(propertySchema)]));
|
|
101
|
+
normalized.required = Object.keys(normalized.properties);
|
|
102
|
+
}
|
|
103
|
+
if (normalized.items) normalized.items = Array.isArray(normalized.items) ? normalized.items.map((item) => normalizeJsonSchemaDefinition(item)) : normalizeJsonSchemaDefinition(normalized.items);
|
|
104
|
+
if (normalized.$defs) normalized.$defs = normalizeJsonSchemaMap(normalized.$defs);
|
|
105
|
+
if (normalized.definitions) normalized.definitions = normalizeJsonSchemaMap(normalized.definitions);
|
|
106
|
+
for (const key of [
|
|
107
|
+
"allOf",
|
|
108
|
+
"anyOf",
|
|
109
|
+
"oneOf"
|
|
110
|
+
]) if (normalized[key]) normalized[key] = normalized[key].map((item) => normalizeJsonSchemaDefinition(item));
|
|
111
|
+
if (normalized.not) normalized.not = normalizeJsonSchemaDefinition(normalized.not);
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
function normalizeJsonSchemaMap(map) {
|
|
115
|
+
return Object.fromEntries(Object.entries(map).map(([key, schema]) => [key, normalizeJsonSchemaDefinition(schema)]));
|
|
116
|
+
}
|
|
117
|
+
function normalizeUsage(usage) {
|
|
118
|
+
const record = asRecord(usage);
|
|
119
|
+
if (!record) return;
|
|
120
|
+
const normalized = {
|
|
121
|
+
inputTokens: numberFromRecord(record, "inputTokens") ?? numberFromRecord(record, "input_tokens") ?? numberFromRecord(record, "prompt_tokens"),
|
|
122
|
+
outputTokens: numberFromRecord(record, "outputTokens") ?? numberFromRecord(record, "output_tokens") ?? numberFromRecord(record, "completion_tokens"),
|
|
123
|
+
totalTokens: numberFromRecord(record, "totalTokens") ?? numberFromRecord(record, "total_tokens")
|
|
124
|
+
};
|
|
125
|
+
return Object.values(normalized).some((value) => value !== void 0) ? normalized : void 0;
|
|
126
|
+
}
|
|
127
|
+
function numberFromRecord(record, key) {
|
|
128
|
+
const value = record[key];
|
|
129
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
130
|
+
}
|
|
131
|
+
function parseStructuredResponse(schema, toolName, response) {
|
|
132
|
+
if (response.finishReason === "content_filter") return {
|
|
133
|
+
error: `${toolName} was not returned because the model finished with content_filter`,
|
|
134
|
+
ok: false,
|
|
135
|
+
retriable: false
|
|
136
|
+
};
|
|
137
|
+
if (response.finishReason === "length") return {
|
|
138
|
+
error: `${toolName} was not returned completely because the model finished with length`,
|
|
139
|
+
ok: false,
|
|
140
|
+
retriable: true
|
|
141
|
+
};
|
|
142
|
+
const toolResults = response.toolResults.filter((result) => result.toolName === toolName);
|
|
143
|
+
if (toolResults.length === 0) return {
|
|
144
|
+
error: `Missing ${toolName} tool result; finishReason=${response.finishReason}`,
|
|
145
|
+
ok: false,
|
|
146
|
+
retriable: true
|
|
147
|
+
};
|
|
148
|
+
if (toolResults.length > 1) return {
|
|
149
|
+
error: `Expected one ${toolName} tool result, received ${toolResults.length}`,
|
|
150
|
+
ok: false,
|
|
151
|
+
retriable: true
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
value: parse(schema, toolResults[0].result)
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
error: errorMessageFrom(error) ?? String(error),
|
|
161
|
+
ok: false,
|
|
162
|
+
retriable: true
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function recordAttemptUsage(options, response) {
|
|
167
|
+
const usage = normalizeUsage(response.usage);
|
|
168
|
+
if (!options.metering || !usage) return;
|
|
169
|
+
options.metering.recordUsage({
|
|
170
|
+
inputTokens: usage.inputTokens,
|
|
171
|
+
metadata: { operation: options.operation },
|
|
172
|
+
modelId: options.model.id,
|
|
173
|
+
outputTokens: usage.outputTokens,
|
|
174
|
+
providerId: options.model.provider.id,
|
|
175
|
+
totalTokens: usage.totalTokens
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function retryFeedbackFrom(toolName, error) {
|
|
179
|
+
return [
|
|
180
|
+
"Your previous tool call could not be validated.",
|
|
181
|
+
`Validation error: ${error}`,
|
|
182
|
+
`Call ${toolName} again with arguments that exactly match the tool schema.`
|
|
183
|
+
].join("\n");
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
export { InvalidStructuredOutputError, formatOutputLanguageInstruction, formatSourceWithLineNumbers, generateStructured, normalizeToolJsonSchema, toolParametersFromSchema };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alint-js/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.20",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"types": "./dist/index.d.mts",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"types": "./dist/agent.d.mts",
|
|
12
12
|
"default": "./dist/agent.mjs"
|
|
13
13
|
},
|
|
14
|
+
"./structured-output": {
|
|
15
|
+
"types": "./dist/structured-output.d.mts",
|
|
16
|
+
"default": "./dist/structured-output.mjs"
|
|
17
|
+
},
|
|
14
18
|
"./package.json": "./package.json"
|
|
15
19
|
},
|
|
16
20
|
"files": [
|
|
@@ -18,6 +22,10 @@
|
|
|
18
22
|
],
|
|
19
23
|
"dependencies": {
|
|
20
24
|
"@moeru/std": "^0.1.0-beta.18",
|
|
25
|
+
"@valibot/to-json-schema": "^1.7.1",
|
|
26
|
+
"@xsai/generate-text": "^0.5.0-beta.6",
|
|
27
|
+
"@xsai/shared-chat": "^0.5.0-beta.6",
|
|
28
|
+
"@xsai/tool": "^0.5.0-beta.6",
|
|
21
29
|
"es-toolkit": "^1.49.0",
|
|
22
30
|
"minimatch": "^10.2.5",
|
|
23
31
|
"oxc-parser": "^0.137.0",
|