@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 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 execute-only `runGoal` control-flow helpers
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",
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.2"
16
+ "@neutrome/lil-engine": "0.3.4"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^25.9.3",
@@ -1,5 +1,6 @@
1
- import type { Program } from "@neutrome/lil-engine";
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 GoalOptions = {
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 runGoal(options: GoalOptions): ExecuteOnly {
129
+ export function createGoalExecutor(options: GoalExecutorOptions): Executor {
131
130
  const maxIterations = positiveInteger(
132
131
  options.maxIterations ?? 3,
133
132
  "goal maxIterations",
134
133
  );
135
- const executeGoal: Executor["execute"] = async (request, ctx) => {
136
- let current = request;
137
- let answer = request;
138
-
139
- for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
140
- answer = await options.draft.execute(current, childContext(ctx, attempt));
141
- const review = await options.review.execute(
142
- answer,
143
- childContext(ctx, attempt),
144
- );
145
- if (await options.satisfied(review, attempt)) {
146
- return answer;
147
- }
148
- if (attempt < maxIterations) {
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
- return answer;
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 { execute: executeGoal };
190
+ return transcript
191
+ ? appendAssistantMessage(createProgram(), transcript)
192
+ : final;
157
193
  }
158
194
 
159
195
  async function runRetry<T>(
@@ -22,8 +22,12 @@ import {
22
22
  appendAssistantMessage,
23
23
  appendToolInteraction,
24
24
  } from "../src/synthetic/index.ts";
25
- import { observeExecutionStream, writeReasoning } from "../src/stream/index.ts";
26
- import { fallback, retry, runGoal } from "../src/loops/index.ts";
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 execute-only goal loops until the review is satisfied", async () => {
520
+ it("runs goal loops until the review is satisfied", async () => {
517
521
  let attempts = 0;
518
- const executor = runGoal({
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(