@neutrome/lilsdk 0.3.5

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 ADDED
@@ -0,0 +1,31 @@
1
+ # `@neutrome/lilsdk`
2
+
3
+ High-level TypeScript SDK for transport-neutral model authoring on top of `@neutrome/lil-engine`.
4
+
5
+ ```ts
6
+ import { createTwoPassExecutor, type Executor } from "@neutrome/lilsdk";
7
+ import { retry } from "@neutrome/lilsdk/loops";
8
+ import {
9
+ observeExecutionStream,
10
+ writeReasoning,
11
+ } from "@neutrome/lilsdk/stream";
12
+ import { connectTools } from "@neutrome/lilsdk/tools";
13
+ ```
14
+
15
+ This package owns:
16
+
17
+ - executor and tool contracts
18
+ - `@neutrome/lilsdk/primitives`: structural program helpers such as `walk`, `map`, `reduce`, `has`, `find`, `indexOf`, `all`, and `any`
19
+ - `@neutrome/lilsdk/synthetic`: request/message synthesis helpers such as `createModelProgram`, `makeMessage`, `makeToolCall`, `makeToolResponse`, and `appendToolInteraction`
20
+ - `@neutrome/lilsdk/tools`: `connectTools` tool-call loop
21
+ - `@neutrome/lilsdk/stream`: stream observation and output helpers
22
+ - `@neutrome/lilsdk/loops`: `retry`, `fallback`, and `createGoalExecutor` control-flow helpers
23
+ - `@neutrome/lilsdk/managed/twoPassExecutor`: `createTwoPassExecutor` and model invocation helpers
24
+ - multi-pass authoring primitives used by virtual models
25
+
26
+ The root export is intentionally small: SDK contracts plus the most common
27
+ high-level two-pass executor helper.
28
+
29
+ It intentionally does not own HTTP routing, provider credentials, target
30
+ resolution, or provider JSON conversion. Those live in `@neutrome/open-ai-router`
31
+ and `@neutrome/lil-engine`.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@neutrome/lilsdk",
3
+ "version": "0.3.5",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./types": "./src/types.ts",
8
+ "./primitives": "./src/primitives/index.ts",
9
+ "./synthetic": "./src/synthetic/index.ts",
10
+ "./stream": "./src/stream/index.ts",
11
+ "./tools": "./src/tools.ts",
12
+ "./loops": "./src/loops/index.ts",
13
+ "./managed": "./src/managed/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "@neutrome/lil-engine": "0.3.4"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^25.9.3",
20
+ "typescript": "^6.0.3",
21
+ "vitest": "^4.1.6"
22
+ },
23
+ "scripts": {
24
+ "test": "vitest run",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type {
2
+ ExecutionTarget,
3
+ Executor,
4
+ ExecutorContext,
5
+ InvokeOptions,
6
+ OutputSink,
7
+ ProgramTransform,
8
+ ExecutionEvent,
9
+ ExecutionEventInput,
10
+ Tool,
11
+ TransformCapability,
12
+ TransformContext,
13
+ } from "./types.ts";
14
+ export { createExecutionEvent } from "./types.ts";
@@ -0,0 +1,266 @@
1
+ import { createProgram, deltaText, type Program } from "@neutrome/lil-engine";
2
+ import type { Executor, ExecutorContext } from "../types.ts";
3
+ import { appendAssistantMessage } from "../synthetic/index.ts";
4
+
5
+ export type RetryOptions = {
6
+ attempts?: number;
7
+ shouldRetry?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
8
+ onRetry?: (error: unknown, attempt: number) => void | Promise<void>;
9
+ };
10
+
11
+ export type FallbackOptions = {
12
+ shouldFallback?: (
13
+ error: unknown,
14
+ executorIndex: number,
15
+ ) => boolean | Promise<boolean>;
16
+ onFallback?: (error: unknown, executorIndex: number) => void | Promise<void>;
17
+ };
18
+
19
+ export type GoalExecutorOptions = {
20
+ draft: Executor;
21
+ review: Executor;
22
+ refine: (
23
+ request: Program,
24
+ answer: Program,
25
+ review: Program,
26
+ attempt: number,
27
+ ) => Program | Promise<Program>;
28
+ satisfied: (review: Program, attempt: number) => boolean | Promise<boolean>;
29
+ maxIterations?: number;
30
+ };
31
+
32
+ export function retry(
33
+ executor: Executor,
34
+ options: RetryOptions = {},
35
+ ): Executor {
36
+ const attempts = positiveInteger(options.attempts ?? 2, "retry attempts");
37
+
38
+ return {
39
+ execute(request, ctx) {
40
+ return runRetry(
41
+ (attempt) => executor.execute(request, childContext(ctx, attempt)),
42
+ attempts,
43
+ options,
44
+ );
45
+ },
46
+
47
+ async *stream(request, ctx) {
48
+ let lastError: unknown;
49
+
50
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
51
+ let emitted = false;
52
+ try {
53
+ for await (const chunk of executor.stream(
54
+ request,
55
+ childContext(ctx, attempt),
56
+ )) {
57
+ emitted = true;
58
+ yield chunk;
59
+ }
60
+ return;
61
+ } catch (error) {
62
+ lastError = error;
63
+ if (emitted) throw error;
64
+ const canRetry =
65
+ attempt < attempts &&
66
+ (await shouldContinue(error, attempt, options.shouldRetry));
67
+ if (!canRetry) {
68
+ throw error;
69
+ }
70
+ await options.onRetry?.(error, attempt);
71
+ }
72
+ }
73
+
74
+ throw lastError;
75
+ },
76
+ };
77
+ }
78
+
79
+ export function fallback(
80
+ executors: readonly Executor[],
81
+ options: FallbackOptions = {},
82
+ ): Executor {
83
+ if (executors.length === 0) {
84
+ throw new Error("fallback requires at least one executor");
85
+ }
86
+
87
+ return {
88
+ execute(request, ctx) {
89
+ return runFallback(
90
+ (executor, index) =>
91
+ executor.execute(request, childContext(ctx, index + 1)),
92
+ executors,
93
+ options,
94
+ );
95
+ },
96
+
97
+ async *stream(request, ctx) {
98
+ let lastError: unknown;
99
+
100
+ for (const [index, executor] of executors.entries()) {
101
+ let emitted = false;
102
+ try {
103
+ for await (const chunk of executor.stream(
104
+ request,
105
+ childContext(ctx, index + 1),
106
+ )) {
107
+ emitted = true;
108
+ yield chunk;
109
+ }
110
+ return;
111
+ } catch (error) {
112
+ lastError = error;
113
+ if (emitted) throw error;
114
+ const canFallback =
115
+ index < executors.length - 1 &&
116
+ (await shouldContinue(error, index, options.shouldFallback));
117
+ if (!canFallback) {
118
+ throw error;
119
+ }
120
+ await options.onFallback?.(error, index);
121
+ }
122
+ }
123
+
124
+ throw lastError;
125
+ },
126
+ };
127
+ }
128
+
129
+ export function createGoalExecutor(options: GoalExecutorOptions): Executor {
130
+ const maxIterations = positiveInteger(
131
+ options.maxIterations ?? 3,
132
+ "goal maxIterations",
133
+ );
134
+
135
+ return {
136
+ async execute(request, ctx) {
137
+ let current = request;
138
+ let answer = request;
139
+
140
+ for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
141
+ const attemptContext = childContext(ctx, attempt);
142
+ answer = await options.draft.execute(current, attemptContext);
143
+ const review = await options.review.execute(answer, attemptContext);
144
+ if (await options.satisfied(review, attempt)) return answer;
145
+ if (attempt < maxIterations) {
146
+ current = await options.refine(current, answer, review, attempt);
147
+ }
148
+ }
149
+
150
+ return answer;
151
+ },
152
+
153
+ async *stream(request, ctx) {
154
+ let current = request;
155
+ let answer = request;
156
+
157
+ for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
158
+ const attemptContext = childContext(ctx, attempt);
159
+ answer = yield* streamGoalStage(
160
+ options.draft.stream(current, attemptContext),
161
+ current,
162
+ );
163
+ const review = yield* streamGoalStage(
164
+ options.review.stream(answer, attemptContext),
165
+ answer,
166
+ );
167
+
168
+ if (await options.satisfied(review, attempt)) return;
169
+ if (attempt < maxIterations) {
170
+ current = await options.refine(current, answer, review, attempt);
171
+ }
172
+ }
173
+ },
174
+ };
175
+ }
176
+
177
+ async function* streamGoalStage(
178
+ source: AsyncIterable<Program>,
179
+ fallback: Program,
180
+ ): AsyncGenerator<Program, Program> {
181
+ let final = fallback;
182
+ let transcript = "";
183
+
184
+ for await (const chunk of source) {
185
+ final = chunk;
186
+ transcript += deltaText(chunk);
187
+ yield chunk;
188
+ }
189
+
190
+ return transcript
191
+ ? appendAssistantMessage(createProgram(), transcript)
192
+ : final;
193
+ }
194
+
195
+ async function runRetry<T>(
196
+ operation: (attempt: number) => Promise<T>,
197
+ attempts: number,
198
+ options: RetryOptions,
199
+ ): Promise<T> {
200
+ let lastError: unknown;
201
+
202
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
203
+ try {
204
+ return await operation(attempt);
205
+ } catch (error) {
206
+ lastError = error;
207
+ const canRetry =
208
+ attempt < attempts &&
209
+ (await shouldContinue(error, attempt, options.shouldRetry));
210
+ if (!canRetry) {
211
+ throw error;
212
+ }
213
+ await options.onRetry?.(error, attempt);
214
+ }
215
+ }
216
+
217
+ throw lastError;
218
+ }
219
+
220
+ async function runFallback<T>(
221
+ operation: (executor: Executor, index: number) => Promise<T>,
222
+ executors: readonly Executor[],
223
+ options: FallbackOptions,
224
+ ): Promise<T> {
225
+ let lastError: unknown;
226
+
227
+ for (const [index, executor] of executors.entries()) {
228
+ try {
229
+ return await operation(executor, index);
230
+ } catch (error) {
231
+ lastError = error;
232
+ const canFallback =
233
+ index < executors.length - 1 &&
234
+ (await shouldContinue(error, index, options.shouldFallback));
235
+ if (!canFallback) {
236
+ throw error;
237
+ }
238
+ await options.onFallback?.(error, index);
239
+ }
240
+ }
241
+
242
+ throw lastError;
243
+ }
244
+
245
+ async function shouldContinue(
246
+ error: unknown,
247
+ attempt: number,
248
+ predicate?: (error: unknown, attempt: number) => boolean | Promise<boolean>,
249
+ ): Promise<boolean> {
250
+ return predicate ? predicate(error, attempt) : true;
251
+ }
252
+
253
+ function childContext(ctx: ExecutorContext, attempt: number): ExecutorContext {
254
+ return {
255
+ ...ctx,
256
+ executionId: `${ctx.executionId}:${attempt}`,
257
+ parentExecutionId: ctx.executionId,
258
+ };
259
+ }
260
+
261
+ function positiveInteger(value: number, label: string): number {
262
+ if (!Number.isInteger(value) || value < 1) {
263
+ throw new Error(`${label} must be a positive integer`);
264
+ }
265
+ return value;
266
+ }
@@ -0,0 +1,18 @@
1
+ export { createTargetExecutor } from "./target-executor.ts";
2
+
3
+ export {
4
+ appendInternalDraft,
5
+ createTwoPassExecutor,
6
+ INTERNAL_DRAFT_CALL_ID,
7
+ INTERNAL_DRAFT_TOOL_NAME,
8
+ invokeExecutor,
9
+ streamExecutor,
10
+ } from "./twoPassExecutor.ts";
11
+
12
+ export type {
13
+ InternalDraft,
14
+ ModelExecutor,
15
+ TwoPassExecutorOptions,
16
+ TwoPassSettings,
17
+ TwoPassSettingsResolver,
18
+ } from "./twoPassExecutor.ts";
@@ -0,0 +1,12 @@
1
+ import type { ExecutionTarget, Executor } from "../types.ts";
2
+
3
+ export function createTargetExecutor(target: ExecutionTarget): Executor {
4
+ return {
5
+ execute(request, context) {
6
+ return context.invoke(request, { target });
7
+ },
8
+ stream(request, context) {
9
+ return context.invokeStream(request, { target });
10
+ },
11
+ };
12
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ appendInstructions,
3
+ contentText,
4
+ insertBefore,
5
+ messageText,
6
+ messages,
7
+ removeRange,
8
+ type Program,
9
+ } from "@neutrome/lil-engine";
10
+ import { Opcode } from "@neutrome/lil-engine";
11
+ import {
12
+ appendToolInteraction,
13
+ makeMessage,
14
+ prependSystemPrompt,
15
+ } from "../synthetic/index.ts";
16
+ import type { ExecutorContext } from "../types.ts";
17
+
18
+ export const INTERNAL_DRAFT_TOOL_NAME = "knowledge";
19
+ export const INTERNAL_DRAFT_CALL_ID = "knowledge_0";
20
+
21
+ export type InternalDraft =
22
+ | string
23
+ | readonly string[]
24
+ | readonly { text: string }[];
25
+
26
+ export type TwoPassSettings = {
27
+ reasoningLevel?: string;
28
+ systemPrompt?: string;
29
+ };
30
+
31
+ export type TwoPassSettingsResolver = (
32
+ request: Program,
33
+ ctx: ExecutorContext,
34
+ ) => TwoPassSettings | null | Promise<TwoPassSettings | null>;
35
+
36
+ export type TwoPassRequestOptions = {
37
+ maxTotalContextLength?: number;
38
+ buildFinalRequest?: (request: Program, draft: string) => Program;
39
+ };
40
+
41
+ export function appendInternalDraft(
42
+ request: Program,
43
+ draft: InternalDraft,
44
+ options: { callId?: string } = {},
45
+ ): Program {
46
+ const callId = options.callId ?? INTERNAL_DRAFT_CALL_ID;
47
+ const drafts = normalizeDrafts(draft);
48
+ if (drafts.length === 0) return request;
49
+ return drafts.reduce(
50
+ (program, text, index) =>
51
+ appendToolInteraction(program, {
52
+ callId: drafts.length === 1 ? callId : `${callId}_${index}`,
53
+ name: INTERNAL_DRAFT_TOOL_NAME,
54
+ args: {},
55
+ result: text,
56
+ }),
57
+ request,
58
+ );
59
+ }
60
+
61
+ export function buildDraftRequest(
62
+ request: Program,
63
+ settings: TwoPassSettings,
64
+ ): Program {
65
+ const withPrompt = settings.systemPrompt
66
+ ? replaceSystemPrompt(request, settings.systemPrompt)
67
+ : request;
68
+ return applySettings(withPrompt, settings);
69
+ }
70
+
71
+ export function buildFinalRequest(
72
+ options: TwoPassRequestOptions,
73
+ request: Program,
74
+ settings: TwoPassSettings,
75
+ draft: string,
76
+ ): Program {
77
+ const limitedDraft = limitDraft(options, request, settings, draft);
78
+ const withDraft = options.buildFinalRequest
79
+ ? options.buildFinalRequest(request, limitedDraft)
80
+ : appendInternalDraft(request, limitedDraft);
81
+ const withPrompt = settings.systemPrompt
82
+ ? prependSystemPrompt(withDraft, settings.systemPrompt)
83
+ : withDraft;
84
+ return applySettings(withPrompt, settings);
85
+ }
86
+
87
+ export function validateMaxContextLength(value: number | undefined): void {
88
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
89
+ throw new Error("maxTotalContextLength must be a non-negative integer");
90
+ }
91
+ }
92
+
93
+ function replaceSystemPrompt(request: Program, systemPrompt: string): Program {
94
+ const systemSpans = messages(request).filter(
95
+ (span) => span.role === "system",
96
+ );
97
+ const previousSystemPrompt = systemSpans
98
+ .map((span) => messageText(request, span).trim())
99
+ .filter(Boolean)
100
+ .join("\n\n");
101
+ let draft = request;
102
+ for (const span of systemSpans.slice().reverse()) {
103
+ draft = removeRange(draft, span.start, span.end);
104
+ }
105
+ draft = prependSystemPrompt(draft, systemPrompt);
106
+ if (!previousSystemPrompt) return draft;
107
+ const firstNonSystem = messages(draft).find((span) => span.role !== "system");
108
+ return insertBefore(
109
+ draft,
110
+ firstNonSystem?.start ?? draft.code.length,
111
+ makeMessage("user", previousSystemPrompt).code,
112
+ );
113
+ }
114
+
115
+ function applySettings(request: Program, settings: TwoPassSettings): Program {
116
+ if (!settings.reasoningLevel) return request;
117
+ return appendInstructions(
118
+ {
119
+ ...request,
120
+ code: request.code.filter(
121
+ (instruction) => instruction.opcode !== Opcode.SET_REASON_EFFORT,
122
+ ),
123
+ },
124
+ [
125
+ {
126
+ opcode: Opcode.SET_REASON_EFFORT,
127
+ value: { kind: "string", value: settings.reasoningLevel },
128
+ },
129
+ ],
130
+ );
131
+ }
132
+
133
+ function limitDraft(
134
+ options: TwoPassRequestOptions,
135
+ request: Program,
136
+ settings: TwoPassSettings,
137
+ draft: string,
138
+ ): string {
139
+ if (options.maxTotalContextLength === undefined) return draft;
140
+ const base = buildFinalRequestWithoutDraft(options, request, settings);
141
+ const available = options.maxTotalContextLength - contentText(base).length;
142
+ return available > 0 ? draft.slice(0, available) : "";
143
+ }
144
+
145
+ function buildFinalRequestWithoutDraft(
146
+ options: TwoPassRequestOptions,
147
+ request: Program,
148
+ settings: TwoPassSettings,
149
+ ): Program {
150
+ const withDraft = options.buildFinalRequest
151
+ ? options.buildFinalRequest(request, "")
152
+ : request;
153
+ const withPrompt = settings.systemPrompt
154
+ ? prependSystemPrompt(withDraft, settings.systemPrompt)
155
+ : withDraft;
156
+ return applySettings(withPrompt, settings);
157
+ }
158
+
159
+ function normalizeDrafts(draft: InternalDraft): string[] {
160
+ const drafts = Array.isArray(draft)
161
+ ? draft.map((item) => (typeof item === "string" ? item : item.text))
162
+ : [draft];
163
+ return drafts.map((text) => text.trim()).filter((text) => text.length > 0);
164
+ }