@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/package.json +2 -2
- package/src/index.ts +3 -2
- package/src/loops/index.ts +24 -18
- package/src/managed/capabilities.ts +215 -0
- package/src/managed/index.ts +4 -6
- package/src/managed/{twoPassExecutor.ts → two-pass.ts} +19 -48
- package/src/stream/index.ts +1 -0
- package/src/stream/stages.ts +25 -0
- package/src/tools.ts +10 -5
- package/src/types.ts +18 -30
- package/test/lilsdk-ts.test.ts +149 -32
- package/test/tools.test.ts +190 -194
- package/src/managed/target-executor.ts +0 -12
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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;
|
package/test/lilsdk-ts.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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("
|
|
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
|
|
331
|
+
const first = await ctx.invoke("smart-model", request);
|
|
331
332
|
const chunks: Program[] = [];
|
|
332
|
-
for await (const chunk of
|
|
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
|
|
350
|
+
const custom = await ctx.invoke(customExecutor, request);
|
|
350
351
|
expect(contentText(custom)).toBe("custom answer");
|
|
351
352
|
|
|
352
353
|
const twoPass = createTwoPassExecutor({
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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(
|
|
625
|
-
return
|
|
721
|
+
async invoke(_executor, request) {
|
|
722
|
+
return request;
|
|
626
723
|
},
|
|
627
|
-
async *invokeStream(
|
|
628
|
-
yield
|
|
724
|
+
async *invokeStream(_executor, request) {
|
|
725
|
+
yield request;
|
|
629
726
|
},
|
|
630
727
|
},
|
|
631
728
|
): ExecutorContext {
|
|
632
|
-
|
|
729
|
+
const context: ExecutorContext = {
|
|
730
|
+
cache: testCache,
|
|
633
731
|
requestId: "req_test",
|
|
634
732
|
executionId: "exec_test",
|
|
635
|
-
invoke
|
|
636
|
-
|
|
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,
|