@neutrome/lilsdk 0.3.5 → 0.4.0

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/src/types.ts CHANGED
@@ -8,20 +8,6 @@ export type TransformCapability =
8
8
  | "drop_content"
9
9
  | "provider_extension";
10
10
 
11
- export type ExecutionTarget =
12
- | {
13
- kind: "provider";
14
- provider?: string;
15
- model: string;
16
- transforms?: string[];
17
- }
18
- | {
19
- kind: "executor";
20
- executorId: string;
21
- alias: string;
22
- transforms?: string[];
23
- };
24
-
25
11
  export type ExecutionEvent = {
26
12
  kind: string;
27
13
  requestId: string;
@@ -37,40 +23,40 @@ export type ExecutionEventInput = Omit<ExecutionEvent, "timestamp"> & {
37
23
  timestamp?: string;
38
24
  };
39
25
 
40
- export type InvokeOptions = {
41
- target?: ExecutionTarget;
42
- requestId?: string;
43
- executionId?: string;
44
- parentExecutionId?: string;
26
+ export type CachePutOptions = {
27
+ expiration?: number;
28
+ expirationTtl?: number;
29
+ };
30
+
31
+ export type Cache = {
32
+ get(key: string): Promise<string | null>;
33
+ put(key: string, value: string, options?: CachePutOptions): Promise<void>;
34
+ delete(key: string): Promise<void>;
45
35
  };
46
36
 
47
- export type TransformContext = {
48
- invoke(request: Program, options?: InvokeOptions): Promise<Program>;
37
+ type RuntimeContext = {
38
+ cache: Cache;
39
+ invoke(executor: ExecutorInput, request: Program): Promise<Program>;
49
40
  invokeStream(
41
+ executor: ExecutorInput,
50
42
  request: Program,
51
- options?: InvokeOptions,
52
43
  ): AsyncIterable<Program>;
53
44
  observe(event: ExecutionEvent): void;
54
45
  signal: AbortSignal;
55
46
  };
56
47
 
48
+ export type TransformContext = RuntimeContext;
49
+
57
50
  export type ProgramTransform = {
58
51
  name: string;
59
52
  capabilities: TransformCapability[];
60
53
  apply(program: Program, ctx: TransformContext): Program | Promise<Program>;
61
54
  };
62
55
 
63
- export type ExecutorContext = {
56
+ export type ExecutorContext = RuntimeContext & {
64
57
  requestId: string;
65
58
  executionId: string;
66
59
  parentExecutionId?: string;
67
- invoke(request: Program, options?: InvokeOptions): Promise<Program>;
68
- invokeStream(
69
- request: Program,
70
- options?: InvokeOptions,
71
- ): AsyncIterable<Program>;
72
- observe(event: ExecutionEvent): void;
73
- signal: AbortSignal;
74
60
  };
75
61
 
76
62
  export type Executor = {
@@ -78,6 +64,8 @@ export type Executor = {
78
64
  stream(request: Program, ctx: ExecutorContext): AsyncIterable<Program>;
79
65
  };
80
66
 
67
+ export type ExecutorInput = string | Executor;
68
+
81
69
  export type Tool = {
82
70
  name: string;
83
71
  description: string;
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  contentText,
3
+ createProgram,
4
+ deltaText,
3
5
  emitChatCompletionsRequest,
4
6
  emitChatCompletionsStreamChunk,
5
7
  getModel,
6
8
  parseChatCompletionsRequest,
7
9
  parseChatCompletionsStreamChunk,
10
+ setModel,
8
11
  Opcode,
9
12
  type Program,
10
13
  } from "@neutrome/lil-engine";
@@ -31,10 +34,8 @@ import { createGoalExecutor, fallback, retry } from "../src/loops/index.ts";
31
34
  import {
32
35
  appendInternalDraft,
33
36
  createTwoPassExecutor,
34
- invokeExecutor,
35
37
  INTERNAL_DRAFT_TOOL_NAME,
36
- streamExecutor,
37
- } from "../src/managed/twoPassExecutor.ts";
38
+ } from "../src/managed/two-pass.ts";
38
39
  import type { Executor, ExecutorContext, OutputSink } from "../src/types.ts";
39
40
 
40
41
  const encoder = new TextEncoder();
@@ -197,7 +198,7 @@ describe("@neutrome/lilsdk", () => {
197
198
  );
198
199
  const seen: Program[] = [];
199
200
  const ctx = buildExecutorContext({
200
- async invoke(program) {
201
+ async invoke(_executor, program) {
201
202
  seen.push(program);
202
203
  if (seen.length === 1) {
203
204
  return appendAssistantMessage(
@@ -216,8 +217,8 @@ describe("@neutrome/lilsdk", () => {
216
217
  });
217
218
 
218
219
  const executor = createTwoPassExecutor({
219
- draftModel: "smart-model",
220
- finalModel: "base-model",
220
+ draft: "smart-model",
221
+ final: "base-model",
221
222
  resolveFinalSettings: () => ({
222
223
  reasoningLevel: "high",
223
224
  systemPrompt: "system prompt",
@@ -265,7 +266,7 @@ describe("@neutrome/lilsdk", () => {
265
266
  );
266
267
  const seen: Program[] = [];
267
268
  const ctx = buildExecutorContext({
268
- async invoke(program) {
269
+ async invoke(_executor, program) {
269
270
  seen.push(program);
270
271
  return appendAssistantMessage(
271
272
  { code: [], buffers: [] },
@@ -278,8 +279,8 @@ describe("@neutrome/lilsdk", () => {
278
279
  });
279
280
 
280
281
  const executor = createTwoPassExecutor({
281
- draftModel: "smart-model",
282
- finalModel: "base-model",
282
+ draft: "smart-model",
283
+ final: "base-model",
283
284
  resolveDraftSettings: () => ({ systemPrompt: "draft system" }),
284
285
  });
285
286
 
@@ -295,17 +296,17 @@ describe("@neutrome/lilsdk", () => {
295
296
  ]);
296
297
  });
297
298
 
298
- it("invokes and streams string or custom executors", async () => {
299
+ it("composes model strings and custom executors", async () => {
299
300
  const calls: Array<{ model: string; streaming: boolean }> = [];
300
301
  const ctx = buildExecutorContext({
301
- async invoke(program) {
302
+ async invoke(_executor, program) {
302
303
  calls.push({ model: getModel(program) ?? "", streaming: false });
303
304
  return appendAssistantMessage(
304
305
  { code: [], buffers: [] },
305
306
  "model answer",
306
307
  );
307
308
  },
308
- async *invokeStream(program) {
309
+ async *invokeStream(_executor, program) {
309
310
  calls.push({ model: getModel(program) ?? "", streaming: true });
310
311
  yield parseChatCompletionsStreamChunk(
311
312
  encoder.encode(
@@ -327,9 +328,9 @@ describe("@neutrome/lilsdk", () => {
327
328
  }),
328
329
  ),
329
330
  );
330
- const first = await invokeExecutor(ctx, request, "smart-model");
331
+ const first = await ctx.invoke("smart-model", request);
331
332
  const chunks: Program[] = [];
332
- for await (const chunk of streamExecutor(ctx, request, "base-model")) {
333
+ for await (const chunk of ctx.invokeStream("base-model", request)) {
333
334
  chunks.push(chunk);
334
335
  }
335
336
 
@@ -346,12 +347,12 @@ describe("@neutrome/lilsdk", () => {
346
347
  const customExecutor = executorFromExecute(async () =>
347
348
  appendAssistantMessage({ code: [], buffers: [] }, "custom answer"),
348
349
  );
349
- const custom = await invokeExecutor(ctx, request, customExecutor);
350
+ const custom = await ctx.invoke(customExecutor, request);
350
351
  expect(contentText(custom)).toBe("custom answer");
351
352
 
352
353
  const twoPass = createTwoPassExecutor({
353
- draftModel: customExecutor,
354
- finalModel: customExecutor,
354
+ draft: customExecutor,
355
+ final: customExecutor,
355
356
  });
356
357
  expect(contentText(await twoPass.execute(request, ctx))).toBe(
357
358
  "custom answer",
@@ -369,7 +370,7 @@ describe("@neutrome/lilsdk", () => {
369
370
  );
370
371
  const seen: Program[] = [];
371
372
  const ctx = buildExecutorContext({
372
- async invoke(program) {
373
+ async invoke(_executor, program) {
373
374
  seen.push(program);
374
375
  return appendAssistantMessage(
375
376
  { code: [], buffers: [] },
@@ -382,8 +383,8 @@ describe("@neutrome/lilsdk", () => {
382
383
  });
383
384
 
384
385
  const executor = createTwoPassExecutor({
385
- draftModel: "smart-model",
386
- finalModel: "base-model",
386
+ draft: "smart-model",
387
+ final: "base-model",
387
388
  maxTotalContextLength: 10,
388
389
  resolveDraftSettings: () => ({ systemPrompt: "draft" }),
389
390
  });
@@ -397,8 +398,8 @@ describe("@neutrome/lilsdk", () => {
397
398
  let receivedRequest: Program | undefined;
398
399
  let receivedContext: ExecutorContext | undefined;
399
400
  const directFinal = createTwoPassExecutor({
400
- draftModel: "unused",
401
- finalModel: "base-model",
401
+ draft: "unused",
402
+ final: "base-model",
402
403
  resolveDraftSettings: (incomingRequest, incomingContext) => {
403
404
  receivedRequest = incomingRequest;
404
405
  receivedContext = incomingContext;
@@ -411,14 +412,55 @@ describe("@neutrome/lilsdk", () => {
411
412
  expect(receivedContext).toBe(ctx);
412
413
 
413
414
  const draftOnly = createTwoPassExecutor({
414
- draftModel: "smart-model",
415
- finalModel: "unused",
415
+ draft: "smart-model",
416
+ final: "unused",
416
417
  resolveFinalSettings: () => null,
417
418
  });
418
419
  await draftOnly.execute(request, ctx);
419
420
  expect(getModel(seen.at(-1)!)).toBe("smart-model");
420
421
  });
421
422
 
423
+ it("keeps two-pass drafts inside the final stream", async () => {
424
+ const executor = createTwoPassExecutor({
425
+ draft: "draft",
426
+ final: "final",
427
+ });
428
+ const ctx = buildExecutorContext({
429
+ async invoke() {
430
+ throw new Error("non-streaming path is not used in this test");
431
+ },
432
+ async *invokeStream(executor) {
433
+ const text = executor === "draft" ? "draft" : "final";
434
+ yield createProgram({
435
+ code: [
436
+ {
437
+ opcode: Opcode.STREAM_DELTA,
438
+ value: { kind: "string", value: text },
439
+ },
440
+ {
441
+ opcode: Opcode.RESP_DONE,
442
+ value: { kind: "string", value: "stop" },
443
+ },
444
+ { opcode: Opcode.STREAM_END, value: { kind: "none" } },
445
+ ],
446
+ });
447
+ },
448
+ });
449
+
450
+ const chunks: Program[] = [];
451
+ for await (const chunk of executor.stream({ code: [], buffers: [] }, ctx)) {
452
+ chunks.push(chunk);
453
+ }
454
+ const opcodes = chunks.flatMap((chunk) =>
455
+ chunk.code.map((instruction) => instruction.opcode),
456
+ );
457
+
458
+ expect(
459
+ opcodes.filter((opcode) => opcode === Opcode.STREAM_END),
460
+ ).toHaveLength(1);
461
+ expect(chunks.map(deltaText).filter(Boolean)).toContain("final");
462
+ });
463
+
422
464
  it("writes reasoning helpers to a sink", async () => {
423
465
  const emitted: string[] = [];
424
466
  const sink: OutputSink = {
@@ -582,7 +624,7 @@ describe("@neutrome/lilsdk", () => {
582
624
  chunks.push(chunk);
583
625
  }
584
626
 
585
- expect(chunks.map(contentText)).toEqual([
627
+ expect(chunks.map(contentText).filter(Boolean)).toEqual([
586
628
  "answer 1",
587
629
  "retry",
588
630
  "answer 2",
@@ -590,6 +632,61 @@ describe("@neutrome/lilsdk", () => {
590
632
  ]);
591
633
  });
592
634
 
635
+ it("keeps goal stage completions inside one stream", async () => {
636
+ let attempt = 0;
637
+ const stage = (text: string) =>
638
+ streamingExecutor(async function* () {
639
+ yield createProgram({
640
+ code: [
641
+ {
642
+ opcode: Opcode.STREAM_DELTA,
643
+ value: { kind: "string", value: text },
644
+ },
645
+ {
646
+ opcode: Opcode.RESP_DONE,
647
+ value: { kind: "string", value: "stop" },
648
+ },
649
+ { opcode: Opcode.STREAM_END, value: { kind: "none" } },
650
+ ],
651
+ });
652
+ });
653
+ const executor = createGoalExecutor({
654
+ draft: streamingExecutor(async function* () {
655
+ attempt += 1;
656
+ yield* stage(`answer ${attempt}`).stream(
657
+ { code: [], buffers: [] },
658
+ buildExecutorContext(),
659
+ );
660
+ }),
661
+ review: streamingExecutor(async function* () {
662
+ yield* stage("review").stream(
663
+ { code: [], buffers: [] },
664
+ buildExecutorContext(),
665
+ );
666
+ }),
667
+ satisfied: () => attempt === 2,
668
+ refine: (request) => request,
669
+ });
670
+
671
+ const chunks: Program[] = [];
672
+ for await (const chunk of executor.stream(
673
+ { code: [], buffers: [] },
674
+ buildExecutorContext(),
675
+ )) {
676
+ chunks.push(chunk);
677
+ }
678
+
679
+ const opcodes = chunks.flatMap((chunk) =>
680
+ chunk.code.map((instruction) => instruction.opcode),
681
+ );
682
+ expect(
683
+ opcodes.filter((opcode) => opcode === Opcode.RESP_DONE),
684
+ ).toHaveLength(1);
685
+ expect(
686
+ opcodes.filter((opcode) => opcode === Opcode.STREAM_END),
687
+ ).toHaveLength(1);
688
+ });
689
+
593
690
  it("uses streamed text as the next goal stage's response", async () => {
594
691
  let reviewedAnswer = "";
595
692
  const executor = createGoalExecutor({
@@ -621,24 +718,44 @@ describe("@neutrome/lilsdk", () => {
621
718
 
622
719
  function buildExecutorContext(
623
720
  impl: Pick<ExecutorContext, "invoke" | "invokeStream"> = {
624
- async invoke(program) {
625
- return program;
721
+ async invoke(_executor, request) {
722
+ return request;
626
723
  },
627
- async *invokeStream(program) {
628
- yield program;
724
+ async *invokeStream(_executor, request) {
725
+ yield request;
629
726
  },
630
727
  },
631
728
  ): ExecutorContext {
632
- return {
729
+ const context: ExecutorContext = {
730
+ cache: testCache,
633
731
  requestId: "req_test",
634
732
  executionId: "exec_test",
635
- invoke: impl.invoke,
636
- invokeStream: impl.invokeStream,
733
+ invoke(executor, request) {
734
+ return typeof executor === "string"
735
+ ? impl.invoke(executor, setModel(request, executor))
736
+ : executor.execute(request, context);
737
+ },
738
+ async *invokeStream(executor, request) {
739
+ if (typeof executor === "string") {
740
+ yield* impl.invokeStream(executor, setModel(request, executor));
741
+ return;
742
+ }
743
+ yield* executor.stream(request, context);
744
+ },
637
745
  observe: () => {},
638
746
  signal: new AbortController().signal,
639
747
  };
748
+ return context;
640
749
  }
641
750
 
751
+ const testCache: ExecutorContext["cache"] = {
752
+ async get() {
753
+ return null;
754
+ },
755
+ async put() {},
756
+ async delete() {},
757
+ };
758
+
642
759
  function executorFromExecute(execute: Executor["execute"]): Executor {
643
760
  return {
644
761
  execute,