@neutrome/lilsdk 0.3.5 → 0.4.1
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/attachment-to-text.ts +175 -0
- package/src/managed/capabilities.ts +215 -0
- package/src/managed/index.ts +9 -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 +203 -32
- package/test/tools.test.ts +190 -194
- package/src/managed/target-executor.ts +0 -12
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
callData,
|
|
3
3
|
contentText,
|
|
4
|
+
createProgram,
|
|
4
5
|
deltaText,
|
|
5
6
|
finishReason,
|
|
6
7
|
hasToolDelta,
|
|
7
8
|
lastMessageRole,
|
|
8
|
-
setModel,
|
|
9
9
|
type Program,
|
|
10
10
|
} from "@neutrome/lil-engine";
|
|
11
11
|
import { streamReasoningDelta } from "../output.ts";
|
|
12
|
-
import
|
|
12
|
+
import { completeStream, streamStage } from "../stream/stages.ts";
|
|
13
|
+
import type { Executor, ExecutorContext, ExecutorInput } from "../types.ts";
|
|
13
14
|
import {
|
|
14
15
|
appendInternalDraft,
|
|
15
16
|
buildDraftRequest,
|
|
@@ -34,41 +35,15 @@ export {
|
|
|
34
35
|
INTERNAL_DRAFT_TOOL_NAME,
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
export type ModelExecutor = string | Executor;
|
|
38
|
-
|
|
39
38
|
export type TwoPassExecutorOptions = TwoPassRequestOptions & {
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
draft: ExecutorInput;
|
|
40
|
+
final: ExecutorInput;
|
|
42
41
|
resolveDraftSettings?: TwoPassSettingsResolver;
|
|
43
42
|
resolveFinalSettings?: TwoPassSettingsResolver;
|
|
44
43
|
reasoningIntro?: string;
|
|
45
44
|
reasoningSeparator?: string;
|
|
46
45
|
};
|
|
47
46
|
|
|
48
|
-
export function invokeExecutor(
|
|
49
|
-
ctx: ExecutorContext,
|
|
50
|
-
request: Program,
|
|
51
|
-
executor: ModelExecutor,
|
|
52
|
-
options?: InvokeOptions,
|
|
53
|
-
): Promise<Program> {
|
|
54
|
-
return typeof executor === "string"
|
|
55
|
-
? ctx.invoke(setModel(request, executor), options)
|
|
56
|
-
: executor.execute(request, ctx);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function* streamExecutor(
|
|
60
|
-
ctx: ExecutorContext,
|
|
61
|
-
request: Program,
|
|
62
|
-
executor: ModelExecutor,
|
|
63
|
-
options?: InvokeOptions,
|
|
64
|
-
): AsyncIterable<Program> {
|
|
65
|
-
if (typeof executor === "string") {
|
|
66
|
-
yield* ctx.invokeStream(setModel(request, executor), options);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
yield* executor.stream(request, ctx);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
47
|
export function createTwoPassExecutor(
|
|
73
48
|
options: TwoPassExecutorOptions,
|
|
74
49
|
): Executor {
|
|
@@ -84,14 +59,10 @@ export function createTwoPassExecutor(
|
|
|
84
59
|
}
|
|
85
60
|
const draftRequest = buildDraftRequest(request, settings.draft);
|
|
86
61
|
if (!settings.final || lastMessageRole(request) === "tool") {
|
|
87
|
-
return
|
|
62
|
+
return ctx.invoke(options.draft, draftRequest);
|
|
88
63
|
}
|
|
89
64
|
|
|
90
|
-
const draftResponse = await
|
|
91
|
-
ctx,
|
|
92
|
-
draftRequest,
|
|
93
|
-
options.draftModel,
|
|
94
|
-
);
|
|
65
|
+
const draftResponse = await ctx.invoke(options.draft, draftRequest);
|
|
95
66
|
if (callData(draftResponse).length > 0) return draftResponse;
|
|
96
67
|
|
|
97
68
|
return invokeFinal(
|
|
@@ -111,17 +82,15 @@ export function createTwoPassExecutor(
|
|
|
111
82
|
}
|
|
112
83
|
const draftRequest = buildDraftRequest(request, settings.draft);
|
|
113
84
|
if (!settings.final || lastMessageRole(request) === "tool") {
|
|
114
|
-
yield*
|
|
85
|
+
yield* ctx.invokeStream(options.draft, draftRequest);
|
|
115
86
|
return;
|
|
116
87
|
}
|
|
117
88
|
|
|
118
89
|
let transcript = "";
|
|
119
90
|
let emittedReasoning = false;
|
|
120
91
|
let toolMode = false;
|
|
121
|
-
for await (const chunk of
|
|
122
|
-
ctx,
|
|
123
|
-
draftRequest,
|
|
124
|
-
options.draftModel,
|
|
92
|
+
for await (const chunk of streamStage(
|
|
93
|
+
ctx.invokeStream(options.draft, draftRequest),
|
|
125
94
|
)) {
|
|
126
95
|
if (ctx.signal.aborted) return;
|
|
127
96
|
if (toolMode) {
|
|
@@ -143,7 +112,11 @@ export function createTwoPassExecutor(
|
|
|
143
112
|
yield streamReasoningDelta(text);
|
|
144
113
|
}
|
|
145
114
|
|
|
146
|
-
if (
|
|
115
|
+
if (ctx.signal.aborted) return;
|
|
116
|
+
if (toolMode) {
|
|
117
|
+
yield completeStream("tool_calls");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
147
120
|
if (emittedReasoning && reasoningSeparator) {
|
|
148
121
|
yield streamReasoningDelta(reasoningSeparator);
|
|
149
122
|
}
|
|
@@ -178,10 +151,9 @@ function invokeFinal(
|
|
|
178
151
|
settings: TwoPassSettings,
|
|
179
152
|
draft = "",
|
|
180
153
|
): Promise<Program> {
|
|
181
|
-
return
|
|
182
|
-
|
|
154
|
+
return ctx.invoke(
|
|
155
|
+
options.final,
|
|
183
156
|
buildFinalRequest(options, request, settings, draft),
|
|
184
|
-
options.finalModel,
|
|
185
157
|
);
|
|
186
158
|
}
|
|
187
159
|
|
|
@@ -192,9 +164,8 @@ async function* streamFinal(
|
|
|
192
164
|
settings: TwoPassSettings,
|
|
193
165
|
draft = "",
|
|
194
166
|
): AsyncIterable<Program> {
|
|
195
|
-
yield*
|
|
196
|
-
|
|
167
|
+
yield* ctx.invokeStream(
|
|
168
|
+
options.final,
|
|
197
169
|
buildFinalRequest(options, request, settings, draft),
|
|
198
|
-
options.finalModel,
|
|
199
170
|
);
|
|
200
171
|
}
|
package/src/stream/index.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createProgram, Opcode, type Program } from "@neutrome/lil-engine";
|
|
2
|
+
|
|
3
|
+
/** Streams one stage without allowing it to complete the enclosing stream. */
|
|
4
|
+
export async function* streamStage(
|
|
5
|
+
source: AsyncIterable<Program>,
|
|
6
|
+
): AsyncGenerator<Program> {
|
|
7
|
+
for await (const chunk of source) {
|
|
8
|
+
const code = chunk.code.filter(
|
|
9
|
+
(instruction) =>
|
|
10
|
+
instruction.opcode !== Opcode.RESP_DONE &&
|
|
11
|
+
instruction.opcode !== Opcode.STREAM_END,
|
|
12
|
+
);
|
|
13
|
+
if (code.length === 0) continue;
|
|
14
|
+
yield code.length === chunk.code.length ? chunk : { ...chunk, code };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function completeStream(reason = "stop"): Program {
|
|
19
|
+
return createProgram({
|
|
20
|
+
code: [
|
|
21
|
+
{ opcode: Opcode.RESP_DONE, value: { kind: "string", value: reason } },
|
|
22
|
+
{ opcode: Opcode.STREAM_END, value: { kind: "none" } },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
type StreamToolDelta,
|
|
11
11
|
} from "@neutrome/lil-engine";
|
|
12
12
|
import { appendToolInteraction } from "./synthetic/index.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type Executor,
|
|
15
|
+
type ExecutorContext,
|
|
16
|
+
type ExecutorInput,
|
|
17
|
+
type Tool,
|
|
18
|
+
} from "./types.ts";
|
|
14
19
|
import {
|
|
15
20
|
buildCallExecutor,
|
|
16
21
|
buildToolAugmenter,
|
|
@@ -176,7 +181,7 @@ async function* streamToolLoop(
|
|
|
176
181
|
|
|
177
182
|
export function connectTools(
|
|
178
183
|
tools: Tool[],
|
|
179
|
-
inner:
|
|
184
|
+
inner: ExecutorInput,
|
|
180
185
|
options: WithToolsOptions = {},
|
|
181
186
|
): Executor {
|
|
182
187
|
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
@@ -192,7 +197,7 @@ export function connectTools(
|
|
|
192
197
|
);
|
|
193
198
|
|
|
194
199
|
while (canRunToolLoop(state, maxIterations)) {
|
|
195
|
-
const response = await
|
|
200
|
+
const response = await ctx.invoke(inner, state.request);
|
|
196
201
|
const calls = callData(response);
|
|
197
202
|
const decision = decideToolLoop(calls, toolMap);
|
|
198
203
|
if (decision.kind !== "continue") return response;
|
|
@@ -205,7 +210,7 @@ export function connectTools(
|
|
|
205
210
|
);
|
|
206
211
|
}
|
|
207
212
|
|
|
208
|
-
return
|
|
213
|
+
return ctx.invoke(inner, state.request);
|
|
209
214
|
},
|
|
210
215
|
|
|
211
216
|
async *stream(request, ctx) {
|
|
@@ -216,7 +221,7 @@ export function connectTools(
|
|
|
216
221
|
executeConnectedCalls,
|
|
217
222
|
request,
|
|
218
223
|
ctx,
|
|
219
|
-
(req) =>
|
|
224
|
+
(req) => ctx.invokeStream(inner, req),
|
|
220
225
|
);
|
|
221
226
|
},
|
|
222
227
|
};
|
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,15 @@
|
|
|
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
|
+
programAttachments,
|
|
11
|
+
viewProgram,
|
|
12
|
+
setModel,
|
|
8
13
|
Opcode,
|
|
9
14
|
type Program,
|
|
10
15
|
} from "@neutrome/lil-engine";
|
|
@@ -31,16 +36,66 @@ import { createGoalExecutor, fallback, retry } from "../src/loops/index.ts";
|
|
|
31
36
|
import {
|
|
32
37
|
appendInternalDraft,
|
|
33
38
|
createTwoPassExecutor,
|
|
34
|
-
invokeExecutor,
|
|
35
39
|
INTERNAL_DRAFT_TOOL_NAME,
|
|
36
|
-
|
|
37
|
-
} from "../src/managed/
|
|
40
|
+
} from "../src/managed/two-pass.ts";
|
|
41
|
+
import { createAttachmentToTextExecutor } from "../src/managed/attachment-to-text.ts";
|
|
38
42
|
import type { Executor, ExecutorContext, OutputSink } from "../src/types.ts";
|
|
39
43
|
|
|
40
44
|
const encoder = new TextEncoder();
|
|
41
45
|
const decoder = new TextDecoder();
|
|
42
46
|
|
|
43
47
|
describe("@neutrome/lilsdk", () => {
|
|
48
|
+
it("describes the latest attachment and removes it before the inner executor", async () => {
|
|
49
|
+
const request = parseChatCompletionsRequest(
|
|
50
|
+
encoder.encode(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
messages: [
|
|
53
|
+
{
|
|
54
|
+
role: "user",
|
|
55
|
+
content: [
|
|
56
|
+
{ type: "text", text: "What is this?" },
|
|
57
|
+
{
|
|
58
|
+
type: "image_url",
|
|
59
|
+
image_url: { url: "data:image/png;base64,aGVsbG8=" },
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
let innerRequest: Program | undefined;
|
|
68
|
+
const executor = createAttachmentToTextExecutor(
|
|
69
|
+
{
|
|
70
|
+
async execute(value) {
|
|
71
|
+
innerRequest = value;
|
|
72
|
+
return value;
|
|
73
|
+
},
|
|
74
|
+
async *stream(value) {
|
|
75
|
+
yield value;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
[
|
|
79
|
+
{
|
|
80
|
+
mimeTypes: ["image/*"],
|
|
81
|
+
executor: {
|
|
82
|
+
async execute() {
|
|
83
|
+
return appendAssistantMessage(
|
|
84
|
+
{ code: [], buffers: [] },
|
|
85
|
+
"A tiny image.",
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
async *stream() {},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
);
|
|
93
|
+
await executor.execute(request, buildExecutorContext());
|
|
94
|
+
expect(programAttachments(innerRequest!)).toHaveLength(0);
|
|
95
|
+
expect(
|
|
96
|
+
viewProgram(innerRequest!).messages.at(-1)?.toolResult?.text,
|
|
97
|
+
).toContain("The original binary media is not present in this context.");
|
|
98
|
+
});
|
|
44
99
|
it("provides clone-safe structural primitives", () => {
|
|
45
100
|
const needle = {
|
|
46
101
|
opcode: Opcode.MSG_START,
|
|
@@ -197,7 +252,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
197
252
|
);
|
|
198
253
|
const seen: Program[] = [];
|
|
199
254
|
const ctx = buildExecutorContext({
|
|
200
|
-
async invoke(program) {
|
|
255
|
+
async invoke(_executor, program) {
|
|
201
256
|
seen.push(program);
|
|
202
257
|
if (seen.length === 1) {
|
|
203
258
|
return appendAssistantMessage(
|
|
@@ -216,8 +271,8 @@ describe("@neutrome/lilsdk", () => {
|
|
|
216
271
|
});
|
|
217
272
|
|
|
218
273
|
const executor = createTwoPassExecutor({
|
|
219
|
-
|
|
220
|
-
|
|
274
|
+
draft: "smart-model",
|
|
275
|
+
final: "base-model",
|
|
221
276
|
resolveFinalSettings: () => ({
|
|
222
277
|
reasoningLevel: "high",
|
|
223
278
|
systemPrompt: "system prompt",
|
|
@@ -265,7 +320,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
265
320
|
);
|
|
266
321
|
const seen: Program[] = [];
|
|
267
322
|
const ctx = buildExecutorContext({
|
|
268
|
-
async invoke(program) {
|
|
323
|
+
async invoke(_executor, program) {
|
|
269
324
|
seen.push(program);
|
|
270
325
|
return appendAssistantMessage(
|
|
271
326
|
{ code: [], buffers: [] },
|
|
@@ -278,8 +333,8 @@ describe("@neutrome/lilsdk", () => {
|
|
|
278
333
|
});
|
|
279
334
|
|
|
280
335
|
const executor = createTwoPassExecutor({
|
|
281
|
-
|
|
282
|
-
|
|
336
|
+
draft: "smart-model",
|
|
337
|
+
final: "base-model",
|
|
283
338
|
resolveDraftSettings: () => ({ systemPrompt: "draft system" }),
|
|
284
339
|
});
|
|
285
340
|
|
|
@@ -295,17 +350,17 @@ describe("@neutrome/lilsdk", () => {
|
|
|
295
350
|
]);
|
|
296
351
|
});
|
|
297
352
|
|
|
298
|
-
it("
|
|
353
|
+
it("composes model strings and custom executors", async () => {
|
|
299
354
|
const calls: Array<{ model: string; streaming: boolean }> = [];
|
|
300
355
|
const ctx = buildExecutorContext({
|
|
301
|
-
async invoke(program) {
|
|
356
|
+
async invoke(_executor, program) {
|
|
302
357
|
calls.push({ model: getModel(program) ?? "", streaming: false });
|
|
303
358
|
return appendAssistantMessage(
|
|
304
359
|
{ code: [], buffers: [] },
|
|
305
360
|
"model answer",
|
|
306
361
|
);
|
|
307
362
|
},
|
|
308
|
-
async *invokeStream(program) {
|
|
363
|
+
async *invokeStream(_executor, program) {
|
|
309
364
|
calls.push({ model: getModel(program) ?? "", streaming: true });
|
|
310
365
|
yield parseChatCompletionsStreamChunk(
|
|
311
366
|
encoder.encode(
|
|
@@ -327,9 +382,9 @@ describe("@neutrome/lilsdk", () => {
|
|
|
327
382
|
}),
|
|
328
383
|
),
|
|
329
384
|
);
|
|
330
|
-
const first = await
|
|
385
|
+
const first = await ctx.invoke("smart-model", request);
|
|
331
386
|
const chunks: Program[] = [];
|
|
332
|
-
for await (const chunk of
|
|
387
|
+
for await (const chunk of ctx.invokeStream("base-model", request)) {
|
|
333
388
|
chunks.push(chunk);
|
|
334
389
|
}
|
|
335
390
|
|
|
@@ -346,12 +401,12 @@ describe("@neutrome/lilsdk", () => {
|
|
|
346
401
|
const customExecutor = executorFromExecute(async () =>
|
|
347
402
|
appendAssistantMessage({ code: [], buffers: [] }, "custom answer"),
|
|
348
403
|
);
|
|
349
|
-
const custom = await
|
|
404
|
+
const custom = await ctx.invoke(customExecutor, request);
|
|
350
405
|
expect(contentText(custom)).toBe("custom answer");
|
|
351
406
|
|
|
352
407
|
const twoPass = createTwoPassExecutor({
|
|
353
|
-
|
|
354
|
-
|
|
408
|
+
draft: customExecutor,
|
|
409
|
+
final: customExecutor,
|
|
355
410
|
});
|
|
356
411
|
expect(contentText(await twoPass.execute(request, ctx))).toBe(
|
|
357
412
|
"custom answer",
|
|
@@ -369,7 +424,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
369
424
|
);
|
|
370
425
|
const seen: Program[] = [];
|
|
371
426
|
const ctx = buildExecutorContext({
|
|
372
|
-
async invoke(program) {
|
|
427
|
+
async invoke(_executor, program) {
|
|
373
428
|
seen.push(program);
|
|
374
429
|
return appendAssistantMessage(
|
|
375
430
|
{ code: [], buffers: [] },
|
|
@@ -382,8 +437,8 @@ describe("@neutrome/lilsdk", () => {
|
|
|
382
437
|
});
|
|
383
438
|
|
|
384
439
|
const executor = createTwoPassExecutor({
|
|
385
|
-
|
|
386
|
-
|
|
440
|
+
draft: "smart-model",
|
|
441
|
+
final: "base-model",
|
|
387
442
|
maxTotalContextLength: 10,
|
|
388
443
|
resolveDraftSettings: () => ({ systemPrompt: "draft" }),
|
|
389
444
|
});
|
|
@@ -397,8 +452,8 @@ describe("@neutrome/lilsdk", () => {
|
|
|
397
452
|
let receivedRequest: Program | undefined;
|
|
398
453
|
let receivedContext: ExecutorContext | undefined;
|
|
399
454
|
const directFinal = createTwoPassExecutor({
|
|
400
|
-
|
|
401
|
-
|
|
455
|
+
draft: "unused",
|
|
456
|
+
final: "base-model",
|
|
402
457
|
resolveDraftSettings: (incomingRequest, incomingContext) => {
|
|
403
458
|
receivedRequest = incomingRequest;
|
|
404
459
|
receivedContext = incomingContext;
|
|
@@ -411,14 +466,55 @@ describe("@neutrome/lilsdk", () => {
|
|
|
411
466
|
expect(receivedContext).toBe(ctx);
|
|
412
467
|
|
|
413
468
|
const draftOnly = createTwoPassExecutor({
|
|
414
|
-
|
|
415
|
-
|
|
469
|
+
draft: "smart-model",
|
|
470
|
+
final: "unused",
|
|
416
471
|
resolveFinalSettings: () => null,
|
|
417
472
|
});
|
|
418
473
|
await draftOnly.execute(request, ctx);
|
|
419
474
|
expect(getModel(seen.at(-1)!)).toBe("smart-model");
|
|
420
475
|
});
|
|
421
476
|
|
|
477
|
+
it("keeps two-pass drafts inside the final stream", async () => {
|
|
478
|
+
const executor = createTwoPassExecutor({
|
|
479
|
+
draft: "draft",
|
|
480
|
+
final: "final",
|
|
481
|
+
});
|
|
482
|
+
const ctx = buildExecutorContext({
|
|
483
|
+
async invoke() {
|
|
484
|
+
throw new Error("non-streaming path is not used in this test");
|
|
485
|
+
},
|
|
486
|
+
async *invokeStream(executor) {
|
|
487
|
+
const text = executor === "draft" ? "draft" : "final";
|
|
488
|
+
yield createProgram({
|
|
489
|
+
code: [
|
|
490
|
+
{
|
|
491
|
+
opcode: Opcode.STREAM_DELTA,
|
|
492
|
+
value: { kind: "string", value: text },
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
opcode: Opcode.RESP_DONE,
|
|
496
|
+
value: { kind: "string", value: "stop" },
|
|
497
|
+
},
|
|
498
|
+
{ opcode: Opcode.STREAM_END, value: { kind: "none" } },
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const chunks: Program[] = [];
|
|
505
|
+
for await (const chunk of executor.stream({ code: [], buffers: [] }, ctx)) {
|
|
506
|
+
chunks.push(chunk);
|
|
507
|
+
}
|
|
508
|
+
const opcodes = chunks.flatMap((chunk) =>
|
|
509
|
+
chunk.code.map((instruction) => instruction.opcode),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
expect(
|
|
513
|
+
opcodes.filter((opcode) => opcode === Opcode.STREAM_END),
|
|
514
|
+
).toHaveLength(1);
|
|
515
|
+
expect(chunks.map(deltaText).filter(Boolean)).toContain("final");
|
|
516
|
+
});
|
|
517
|
+
|
|
422
518
|
it("writes reasoning helpers to a sink", async () => {
|
|
423
519
|
const emitted: string[] = [];
|
|
424
520
|
const sink: OutputSink = {
|
|
@@ -582,7 +678,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
582
678
|
chunks.push(chunk);
|
|
583
679
|
}
|
|
584
680
|
|
|
585
|
-
expect(chunks.map(contentText)).toEqual([
|
|
681
|
+
expect(chunks.map(contentText).filter(Boolean)).toEqual([
|
|
586
682
|
"answer 1",
|
|
587
683
|
"retry",
|
|
588
684
|
"answer 2",
|
|
@@ -590,6 +686,61 @@ describe("@neutrome/lilsdk", () => {
|
|
|
590
686
|
]);
|
|
591
687
|
});
|
|
592
688
|
|
|
689
|
+
it("keeps goal stage completions inside one stream", async () => {
|
|
690
|
+
let attempt = 0;
|
|
691
|
+
const stage = (text: string) =>
|
|
692
|
+
streamingExecutor(async function* () {
|
|
693
|
+
yield createProgram({
|
|
694
|
+
code: [
|
|
695
|
+
{
|
|
696
|
+
opcode: Opcode.STREAM_DELTA,
|
|
697
|
+
value: { kind: "string", value: text },
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
opcode: Opcode.RESP_DONE,
|
|
701
|
+
value: { kind: "string", value: "stop" },
|
|
702
|
+
},
|
|
703
|
+
{ opcode: Opcode.STREAM_END, value: { kind: "none" } },
|
|
704
|
+
],
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
const executor = createGoalExecutor({
|
|
708
|
+
draft: streamingExecutor(async function* () {
|
|
709
|
+
attempt += 1;
|
|
710
|
+
yield* stage(`answer ${attempt}`).stream(
|
|
711
|
+
{ code: [], buffers: [] },
|
|
712
|
+
buildExecutorContext(),
|
|
713
|
+
);
|
|
714
|
+
}),
|
|
715
|
+
review: streamingExecutor(async function* () {
|
|
716
|
+
yield* stage("review").stream(
|
|
717
|
+
{ code: [], buffers: [] },
|
|
718
|
+
buildExecutorContext(),
|
|
719
|
+
);
|
|
720
|
+
}),
|
|
721
|
+
satisfied: () => attempt === 2,
|
|
722
|
+
refine: (request) => request,
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
const chunks: Program[] = [];
|
|
726
|
+
for await (const chunk of executor.stream(
|
|
727
|
+
{ code: [], buffers: [] },
|
|
728
|
+
buildExecutorContext(),
|
|
729
|
+
)) {
|
|
730
|
+
chunks.push(chunk);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const opcodes = chunks.flatMap((chunk) =>
|
|
734
|
+
chunk.code.map((instruction) => instruction.opcode),
|
|
735
|
+
);
|
|
736
|
+
expect(
|
|
737
|
+
opcodes.filter((opcode) => opcode === Opcode.RESP_DONE),
|
|
738
|
+
).toHaveLength(1);
|
|
739
|
+
expect(
|
|
740
|
+
opcodes.filter((opcode) => opcode === Opcode.STREAM_END),
|
|
741
|
+
).toHaveLength(1);
|
|
742
|
+
});
|
|
743
|
+
|
|
593
744
|
it("uses streamed text as the next goal stage's response", async () => {
|
|
594
745
|
let reviewedAnswer = "";
|
|
595
746
|
const executor = createGoalExecutor({
|
|
@@ -621,24 +772,44 @@ describe("@neutrome/lilsdk", () => {
|
|
|
621
772
|
|
|
622
773
|
function buildExecutorContext(
|
|
623
774
|
impl: Pick<ExecutorContext, "invoke" | "invokeStream"> = {
|
|
624
|
-
async invoke(
|
|
625
|
-
return
|
|
775
|
+
async invoke(_executor, request) {
|
|
776
|
+
return request;
|
|
626
777
|
},
|
|
627
|
-
async *invokeStream(
|
|
628
|
-
yield
|
|
778
|
+
async *invokeStream(_executor, request) {
|
|
779
|
+
yield request;
|
|
629
780
|
},
|
|
630
781
|
},
|
|
631
782
|
): ExecutorContext {
|
|
632
|
-
|
|
783
|
+
const context: ExecutorContext = {
|
|
784
|
+
cache: testCache,
|
|
633
785
|
requestId: "req_test",
|
|
634
786
|
executionId: "exec_test",
|
|
635
|
-
invoke
|
|
636
|
-
|
|
787
|
+
invoke(executor, request) {
|
|
788
|
+
return typeof executor === "string"
|
|
789
|
+
? impl.invoke(executor, setModel(request, executor))
|
|
790
|
+
: executor.execute(request, context);
|
|
791
|
+
},
|
|
792
|
+
async *invokeStream(executor, request) {
|
|
793
|
+
if (typeof executor === "string") {
|
|
794
|
+
yield* impl.invokeStream(executor, setModel(request, executor));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
yield* executor.stream(request, context);
|
|
798
|
+
},
|
|
637
799
|
observe: () => {},
|
|
638
800
|
signal: new AbortController().signal,
|
|
639
801
|
};
|
|
802
|
+
return context;
|
|
640
803
|
}
|
|
641
804
|
|
|
805
|
+
const testCache: ExecutorContext["cache"] = {
|
|
806
|
+
async get() {
|
|
807
|
+
return null;
|
|
808
|
+
},
|
|
809
|
+
async put() {},
|
|
810
|
+
async delete() {},
|
|
811
|
+
};
|
|
812
|
+
|
|
642
813
|
function executorFromExecute(execute: Executor["execute"]): Executor {
|
|
643
814
|
return {
|
|
644
815
|
execute,
|