@neutrome/lilsdk-ts 0.3.3 → 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 +1 -1
- package/package.json +2 -2
- package/src/loops/index.ts +59 -23
- package/test/lilsdk-ts.test.ts +76 -4
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ This package owns:
|
|
|
19
19
|
- `@neutrome/lilsdk-ts/synthetic`: request/message synthesis helpers such as `createModelProgram`, `makeMessage`, `makeToolCall`, `makeToolResponse`, and `appendToolInteraction`
|
|
20
20
|
- `@neutrome/lilsdk-ts/tools`: `connectTools` tool-call loop
|
|
21
21
|
- `@neutrome/lilsdk-ts/stream`: stream observation and output helpers
|
|
22
|
-
- `@neutrome/lilsdk-ts/loops`: `retry`, `fallback`, and
|
|
22
|
+
- `@neutrome/lilsdk-ts/loops`: `retry`, `fallback`, and `createGoalExecutor` control-flow helpers
|
|
23
23
|
- `@neutrome/lilsdk-ts/managed/twoPassExecutor`: `createTwoPassExecutor` and model invocation helpers
|
|
24
24
|
- multi-pass authoring primitives used by virtual models
|
|
25
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/lilsdk-ts",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./managed": "./src/managed/index.ts"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@neutrome/lil-engine": "0.3.
|
|
16
|
+
"@neutrome/lil-engine": "0.3.4"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^25.9.3",
|
package/src/loops/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { createProgram, deltaText, type Program } from "@neutrome/lil-engine";
|
|
2
2
|
import type { Executor, ExecutorContext } from "../types.ts";
|
|
3
|
+
import { appendAssistantMessage } from "../synthetic/index.ts";
|
|
3
4
|
|
|
4
5
|
export type RetryOptions = {
|
|
5
6
|
attempts?: number;
|
|
@@ -15,7 +16,7 @@ export type FallbackOptions = {
|
|
|
15
16
|
onFallback?: (error: unknown, executorIndex: number) => void | Promise<void>;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
export type
|
|
19
|
+
export type GoalExecutorOptions = {
|
|
19
20
|
draft: Executor;
|
|
20
21
|
review: Executor;
|
|
21
22
|
refine: (
|
|
@@ -28,8 +29,6 @@ export type GoalOptions = {
|
|
|
28
29
|
maxIterations?: number;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
export type ExecuteOnly = Pick<Executor, "execute">;
|
|
32
|
-
|
|
33
32
|
export function retry(
|
|
34
33
|
executor: Executor,
|
|
35
34
|
options: RetryOptions = {},
|
|
@@ -127,33 +126,70 @@ export function fallback(
|
|
|
127
126
|
};
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
export function
|
|
129
|
+
export function createGoalExecutor(options: GoalExecutorOptions): Executor {
|
|
131
130
|
const maxIterations = positiveInteger(
|
|
132
131
|
options.maxIterations ?? 3,
|
|
133
132
|
"goal maxIterations",
|
|
134
133
|
);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
current = await options.refine(current, answer, review, attempt);
|
|
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
|
+
}
|
|
150
148
|
}
|
|
151
|
-
}
|
|
152
149
|
|
|
153
|
-
|
|
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
|
+
},
|
|
154
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
|
+
}
|
|
155
189
|
|
|
156
|
-
return
|
|
190
|
+
return transcript
|
|
191
|
+
? appendAssistantMessage(createProgram(), transcript)
|
|
192
|
+
: final;
|
|
157
193
|
}
|
|
158
194
|
|
|
159
195
|
async function runRetry<T>(
|
package/test/lilsdk-ts.test.ts
CHANGED
|
@@ -22,8 +22,12 @@ import {
|
|
|
22
22
|
appendAssistantMessage,
|
|
23
23
|
appendToolInteraction,
|
|
24
24
|
} from "../src/synthetic/index.ts";
|
|
25
|
-
import {
|
|
26
|
-
|
|
25
|
+
import {
|
|
26
|
+
observeExecutionStream,
|
|
27
|
+
streamTextResponse,
|
|
28
|
+
writeReasoning,
|
|
29
|
+
} from "../src/stream/index.ts";
|
|
30
|
+
import { createGoalExecutor, fallback, retry } from "../src/loops/index.ts";
|
|
27
31
|
import {
|
|
28
32
|
appendInternalDraft,
|
|
29
33
|
createTwoPassExecutor,
|
|
@@ -513,9 +517,9 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
513
517
|
expect(fallbackCalls).toBe(0);
|
|
514
518
|
});
|
|
515
519
|
|
|
516
|
-
it("runs
|
|
520
|
+
it("runs goal loops until the review is satisfied", async () => {
|
|
517
521
|
let attempts = 0;
|
|
518
|
-
const executor =
|
|
522
|
+
const executor = createGoalExecutor({
|
|
519
523
|
draft: executorFromExecute(async () => {
|
|
520
524
|
attempts += 1;
|
|
521
525
|
return appendAssistantMessage(
|
|
@@ -545,6 +549,74 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
545
549
|
expect(attempts).toBe(2);
|
|
546
550
|
expect(contentText(result)).toBe("answer 2");
|
|
547
551
|
});
|
|
552
|
+
|
|
553
|
+
it("streams draft and review passes through each goal iteration", async () => {
|
|
554
|
+
let attempts = 0;
|
|
555
|
+
const executor = createGoalExecutor({
|
|
556
|
+
draft: streamingExecutor(async function* () {
|
|
557
|
+
attempts += 1;
|
|
558
|
+
yield appendAssistantMessage(
|
|
559
|
+
{ code: [], buffers: [] },
|
|
560
|
+
`answer ${attempts}`,
|
|
561
|
+
);
|
|
562
|
+
}),
|
|
563
|
+
review: streamingExecutor(async function* () {
|
|
564
|
+
yield appendAssistantMessage(
|
|
565
|
+
{ code: [], buffers: [] },
|
|
566
|
+
attempts > 1 ? "pass" : "retry",
|
|
567
|
+
);
|
|
568
|
+
}),
|
|
569
|
+
satisfied(review) {
|
|
570
|
+
return contentText(review) === "pass";
|
|
571
|
+
},
|
|
572
|
+
refine(request) {
|
|
573
|
+
return request;
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const chunks: Program[] = [];
|
|
578
|
+
for await (const chunk of executor.stream(
|
|
579
|
+
{ code: [], buffers: [] },
|
|
580
|
+
buildExecutorContext(),
|
|
581
|
+
)) {
|
|
582
|
+
chunks.push(chunk);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
expect(chunks.map(contentText)).toEqual([
|
|
586
|
+
"answer 1",
|
|
587
|
+
"retry",
|
|
588
|
+
"answer 2",
|
|
589
|
+
"pass",
|
|
590
|
+
]);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("uses streamed text as the next goal stage's response", async () => {
|
|
594
|
+
let reviewedAnswer = "";
|
|
595
|
+
const executor = createGoalExecutor({
|
|
596
|
+
draft: streamingExecutor(async function* () {
|
|
597
|
+
yield* streamTextResponse("answer");
|
|
598
|
+
}),
|
|
599
|
+
review: streamingExecutor(async function* (request) {
|
|
600
|
+
reviewedAnswer = contentText(request);
|
|
601
|
+
yield* streamTextResponse("pass");
|
|
602
|
+
}),
|
|
603
|
+
satisfied(review) {
|
|
604
|
+
return contentText(review) === "pass";
|
|
605
|
+
},
|
|
606
|
+
refine(request) {
|
|
607
|
+
return request;
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
for await (const _chunk of executor.stream(
|
|
612
|
+
{ code: [], buffers: [] },
|
|
613
|
+
buildExecutorContext(),
|
|
614
|
+
)) {
|
|
615
|
+
// Consume the entire goal stream.
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
expect(reviewedAnswer).toBe("answer");
|
|
619
|
+
});
|
|
548
620
|
});
|
|
549
621
|
|
|
550
622
|
function buildExecutorContext(
|