@neutrome/lilsdk 0.4.5 → 0.5.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/README.md +24 -21
- package/etc/api/index.api.md +96 -0
- package/etc/api/loops.api.md +108 -0
- package/etc/api/managed.api.md +102 -0
- package/etc/api/primitives.api.md +39 -0
- package/etc/api/stream.api.md +46 -0
- package/etc/api/tools.api.md +103 -0
- package/package.json +8 -9
- package/src/index.ts +41 -15
- package/src/loops/goal.ts +111 -0
- package/src/loops/index.ts +41 -110
- package/src/loops/shared.ts +27 -0
- package/src/managed/attachment-to-text.ts +35 -11
- package/src/managed/capabilities.ts +65 -25
- package/src/managed/index.ts +0 -14
- package/src/observe.ts +21 -6
- package/src/output.ts +13 -25
- package/src/primitives/index.ts +69 -0
- package/src/stream/index.ts +2 -6
- package/src/stream/stages.ts +15 -11
- package/src/tools-support.ts +61 -30
- package/src/tools.ts +37 -20
- package/src/types.ts +129 -4
- package/test/lilsdk-ts.test.ts +71 -341
- package/test/tools.test.ts +69 -21
- package/src/managed/two-pass-request.ts +0 -164
- package/src/managed/two-pass.ts +0 -171
- package/src/synthetic/index.ts +0 -134
package/test/lilsdk-ts.test.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
appendAssistantMessage,
|
|
3
|
+
appendToolInteraction,
|
|
3
4
|
createProgram,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
parseChatCompletionsStreamChunk,
|
|
10
|
-
programAttachments,
|
|
11
|
-
viewProgram,
|
|
12
|
-
setModel,
|
|
5
|
+
delta,
|
|
6
|
+
emitProviderRequest,
|
|
7
|
+
emitProviderStreamChunk,
|
|
8
|
+
extractAttachments,
|
|
9
|
+
extractContentText,
|
|
13
10
|
Opcode,
|
|
11
|
+
parseProviderRequest,
|
|
12
|
+
parseProviderStreamChunk,
|
|
13
|
+
ProgramView,
|
|
14
|
+
setModel,
|
|
14
15
|
type Program,
|
|
15
16
|
} from "@neutrome/lil-engine";
|
|
16
17
|
import { describe, expect, it } from "vitest";
|
|
@@ -23,30 +24,47 @@ import {
|
|
|
23
24
|
map,
|
|
24
25
|
reduce,
|
|
25
26
|
} from "../src/primitives/index.ts";
|
|
26
|
-
import {
|
|
27
|
-
appendAssistantMessage,
|
|
28
|
-
appendToolInteraction,
|
|
29
|
-
} from "../src/synthetic/index.ts";
|
|
30
|
-
import {
|
|
31
|
-
observeExecutionStream,
|
|
32
|
-
streamTextResponse,
|
|
33
|
-
writeReasoning,
|
|
34
|
-
} from "../src/stream/index.ts";
|
|
27
|
+
import { observeExecutionStream, writeThinking } from "../src/stream/index.ts";
|
|
35
28
|
import { createGoalExecutor, fallback, retry } from "../src/loops/index.ts";
|
|
36
|
-
import {
|
|
37
|
-
appendInternalDraft,
|
|
38
|
-
createTwoPassExecutor,
|
|
39
|
-
INTERNAL_DRAFT_TOOL_NAME,
|
|
40
|
-
} from "../src/managed/two-pass.ts";
|
|
41
29
|
import { createAttachmentToTextExecutor } from "../src/managed/attachment-to-text.ts";
|
|
30
|
+
import { createCapabilitiesExecutor } from "../src/managed/capabilities.ts";
|
|
42
31
|
import type { Executor, ExecutorContext, OutputSink } from "../src/types.ts";
|
|
43
32
|
|
|
44
33
|
const encoder = new TextEncoder();
|
|
45
34
|
const decoder = new TextDecoder();
|
|
46
35
|
|
|
47
36
|
describe("@neutrome/lilsdk", () => {
|
|
37
|
+
it("leaves capability tool prefixes directly available", async () => {
|
|
38
|
+
const request = parseProviderRequest(
|
|
39
|
+
"chat-completions",
|
|
40
|
+
encoder.encode(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
tools: [
|
|
43
|
+
{ type: "function", function: { name: "keep_search" } },
|
|
44
|
+
{ type: "function", function: { name: "managed_weather" } },
|
|
45
|
+
],
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
let innerRequest: Program | undefined;
|
|
50
|
+
const executor = createCapabilitiesExecutor(
|
|
51
|
+
executorFromExecute(async (value) => {
|
|
52
|
+
innerRequest = value;
|
|
53
|
+
return value;
|
|
54
|
+
}),
|
|
55
|
+
{ skipPrefixes: ["keep_"] },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await executor.execute(request, buildExecutorContext());
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
new ProgramView(innerRequest!).tools.map((tool) => tool.name),
|
|
62
|
+
).toEqual(["keep_search", "learn_capability"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
48
65
|
it("describes the latest attachment and removes it before the inner executor", async () => {
|
|
49
|
-
const request =
|
|
66
|
+
const request = parseProviderRequest(
|
|
67
|
+
"chat-completions",
|
|
50
68
|
encoder.encode(
|
|
51
69
|
JSON.stringify({
|
|
52
70
|
messages: [
|
|
@@ -91,9 +109,9 @@ describe("@neutrome/lilsdk", () => {
|
|
|
91
109
|
],
|
|
92
110
|
);
|
|
93
111
|
await executor.execute(request, buildExecutorContext());
|
|
94
|
-
expect(
|
|
112
|
+
expect(extractAttachments(innerRequest!)).toHaveLength(0);
|
|
95
113
|
expect(
|
|
96
|
-
|
|
114
|
+
new ProgramView(innerRequest!).messages.at(-1)?.toolResult?.text,
|
|
97
115
|
).toContain("The original binary media is not present in this context.");
|
|
98
116
|
});
|
|
99
117
|
it("provides clone-safe structural primitives", () => {
|
|
@@ -128,7 +146,8 @@ describe("@neutrome/lilsdk", () => {
|
|
|
128
146
|
it("observes tool-call streams", async () => {
|
|
129
147
|
const observed = await observeExecutionStream(
|
|
130
148
|
(async function* () {
|
|
131
|
-
yield
|
|
149
|
+
yield parseProviderStreamChunk(
|
|
150
|
+
"chat-completions",
|
|
132
151
|
encoder.encode(
|
|
133
152
|
JSON.stringify({
|
|
134
153
|
id: "tool-stream",
|
|
@@ -138,7 +157,8 @@ describe("@neutrome/lilsdk", () => {
|
|
|
138
157
|
}),
|
|
139
158
|
),
|
|
140
159
|
);
|
|
141
|
-
yield
|
|
160
|
+
yield parseProviderStreamChunk(
|
|
161
|
+
"chat-completions",
|
|
142
162
|
encoder.encode(
|
|
143
163
|
JSON.stringify({
|
|
144
164
|
id: "tool-stream",
|
|
@@ -162,7 +182,8 @@ describe("@neutrome/lilsdk", () => {
|
|
|
162
182
|
}),
|
|
163
183
|
),
|
|
164
184
|
);
|
|
165
|
-
yield
|
|
185
|
+
yield parseProviderStreamChunk(
|
|
186
|
+
"chat-completions",
|
|
166
187
|
encoder.encode(
|
|
167
188
|
JSON.stringify({
|
|
168
189
|
id: "tool-stream",
|
|
@@ -181,37 +202,18 @@ describe("@neutrome/lilsdk", () => {
|
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
const parsed = observed.chunks.map((chunk) =>
|
|
184
|
-
JSON.parse(
|
|
205
|
+
JSON.parse(
|
|
206
|
+
decoder.decode(emitProviderStreamChunk("chat-completions", chunk)),
|
|
207
|
+
),
|
|
185
208
|
);
|
|
186
209
|
expect(parsed.some((chunk) => chunk.choices?.[0]?.delta?.tool_calls)).toBe(
|
|
187
210
|
true,
|
|
188
211
|
);
|
|
189
212
|
});
|
|
190
213
|
|
|
191
|
-
it("appends internal drafts with fixed SDK metadata and empty args", () => {
|
|
192
|
-
const request = parseChatCompletionsRequest(
|
|
193
|
-
encoder.encode(
|
|
194
|
-
JSON.stringify({
|
|
195
|
-
model: "virtual-model",
|
|
196
|
-
messages: [{ role: "user", content: "hello" }],
|
|
197
|
-
}),
|
|
198
|
-
),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const updated = appendInternalDraft(request, "draft answer");
|
|
202
|
-
const emitted = JSON.parse(
|
|
203
|
-
decoder.decode(emitChatCompletionsRequest(updated)),
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
expect(emitted.messages[1].tool_calls[0].function.name).toBe(
|
|
207
|
-
INTERNAL_DRAFT_TOOL_NAME,
|
|
208
|
-
);
|
|
209
|
-
expect(emitted.messages[1].tool_calls[0].function.arguments).toBe("{}");
|
|
210
|
-
expect(emitted.messages[2].content).toBe("draft answer");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
214
|
it("appends generic synthetic tool interactions", () => {
|
|
214
|
-
const request =
|
|
215
|
+
const request = parseProviderRequest(
|
|
216
|
+
"chat-completions",
|
|
215
217
|
encoder.encode(
|
|
216
218
|
JSON.stringify({
|
|
217
219
|
model: "virtual-model",
|
|
@@ -227,7 +229,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
227
229
|
result: { status: "active" },
|
|
228
230
|
});
|
|
229
231
|
const emitted = JSON.parse(
|
|
230
|
-
decoder.decode(
|
|
232
|
+
decoder.decode(emitProviderRequest("chat-completions", updated)),
|
|
231
233
|
);
|
|
232
234
|
|
|
233
235
|
expect(emitted.messages[1].tool_calls[0].function).toEqual({
|
|
@@ -241,292 +243,20 @@ describe("@neutrome/lilsdk", () => {
|
|
|
241
243
|
});
|
|
242
244
|
});
|
|
243
245
|
|
|
244
|
-
it("builds two-pass executors from model and settings resolvers", async () => {
|
|
245
|
-
const request = parseChatCompletionsRequest(
|
|
246
|
-
encoder.encode(
|
|
247
|
-
JSON.stringify({
|
|
248
|
-
model: "virtual-model",
|
|
249
|
-
messages: [{ role: "user", content: "hello" }],
|
|
250
|
-
}),
|
|
251
|
-
),
|
|
252
|
-
);
|
|
253
|
-
const seen: Program[] = [];
|
|
254
|
-
const ctx = buildExecutorContext({
|
|
255
|
-
async invoke(_executor, program) {
|
|
256
|
-
seen.push(program);
|
|
257
|
-
if (seen.length === 1) {
|
|
258
|
-
return appendAssistantMessage(
|
|
259
|
-
{ code: [], buffers: [] },
|
|
260
|
-
"draft answer",
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
return appendAssistantMessage(
|
|
264
|
-
{ code: [], buffers: [] },
|
|
265
|
-
"final answer",
|
|
266
|
-
);
|
|
267
|
-
},
|
|
268
|
-
async *invokeStream() {
|
|
269
|
-
throw new Error("streaming path is not used in this test");
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const executor = createTwoPassExecutor({
|
|
274
|
-
draft: "smart-model",
|
|
275
|
-
final: "base-model",
|
|
276
|
-
resolveFinalSettings: () => ({
|
|
277
|
-
reasoningLevel: "high",
|
|
278
|
-
systemPrompt: "system prompt",
|
|
279
|
-
}),
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
const result = await executor.execute(request, ctx);
|
|
283
|
-
|
|
284
|
-
expect(seen.map((program) => getModel(program))).toEqual([
|
|
285
|
-
"smart-model",
|
|
286
|
-
"base-model",
|
|
287
|
-
]);
|
|
288
|
-
expect(contentText(result)).toBe("final answer");
|
|
289
|
-
|
|
290
|
-
const finalRequest = JSON.parse(
|
|
291
|
-
decoder.decode(emitChatCompletionsRequest(seen[1]!)),
|
|
292
|
-
);
|
|
293
|
-
expect(finalRequest.messages[0]).toEqual({
|
|
294
|
-
role: "system",
|
|
295
|
-
content: "system prompt",
|
|
296
|
-
});
|
|
297
|
-
expect(finalRequest.reasoning_effort).toBe("high");
|
|
298
|
-
expect(finalRequest.messages[2].tool_calls[0].function).toEqual({
|
|
299
|
-
name: INTERNAL_DRAFT_TOOL_NAME,
|
|
300
|
-
arguments: "{}",
|
|
301
|
-
});
|
|
302
|
-
expect(finalRequest.messages[3]).toEqual({
|
|
303
|
-
role: "tool",
|
|
304
|
-
tool_call_id: "knowledge_0",
|
|
305
|
-
content: "draft answer",
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("replaces draft system prompt and moves existing system prompt to first user message", async () => {
|
|
310
|
-
const request = parseChatCompletionsRequest(
|
|
311
|
-
encoder.encode(
|
|
312
|
-
JSON.stringify({
|
|
313
|
-
model: "virtual-model",
|
|
314
|
-
messages: [
|
|
315
|
-
{ role: "system", content: "original system" },
|
|
316
|
-
{ role: "user", content: "hello" },
|
|
317
|
-
],
|
|
318
|
-
}),
|
|
319
|
-
),
|
|
320
|
-
);
|
|
321
|
-
const seen: Program[] = [];
|
|
322
|
-
const ctx = buildExecutorContext({
|
|
323
|
-
async invoke(_executor, program) {
|
|
324
|
-
seen.push(program);
|
|
325
|
-
return appendAssistantMessage(
|
|
326
|
-
{ code: [], buffers: [] },
|
|
327
|
-
seen.length === 1 ? "draft" : "final",
|
|
328
|
-
);
|
|
329
|
-
},
|
|
330
|
-
async *invokeStream() {
|
|
331
|
-
throw new Error("streaming path is not used in this test");
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
const executor = createTwoPassExecutor({
|
|
336
|
-
draft: "smart-model",
|
|
337
|
-
final: "base-model",
|
|
338
|
-
resolveDraftSettings: () => ({ systemPrompt: "draft system" }),
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
await executor.execute(request, ctx);
|
|
342
|
-
|
|
343
|
-
const draftRequest = JSON.parse(
|
|
344
|
-
decoder.decode(emitChatCompletionsRequest(seen[0]!)),
|
|
345
|
-
);
|
|
346
|
-
expect(draftRequest.messages).toEqual([
|
|
347
|
-
{ role: "system", content: "draft system" },
|
|
348
|
-
{ role: "user", content: "original system" },
|
|
349
|
-
{ role: "user", content: "hello" },
|
|
350
|
-
]);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it("composes model strings and custom executors", async () => {
|
|
354
|
-
const calls: Array<{ model: string; streaming: boolean }> = [];
|
|
355
|
-
const ctx = buildExecutorContext({
|
|
356
|
-
async invoke(_executor, program) {
|
|
357
|
-
calls.push({ model: getModel(program) ?? "", streaming: false });
|
|
358
|
-
return appendAssistantMessage(
|
|
359
|
-
{ code: [], buffers: [] },
|
|
360
|
-
"model answer",
|
|
361
|
-
);
|
|
362
|
-
},
|
|
363
|
-
async *invokeStream(_executor, program) {
|
|
364
|
-
calls.push({ model: getModel(program) ?? "", streaming: true });
|
|
365
|
-
yield parseChatCompletionsStreamChunk(
|
|
366
|
-
encoder.encode(
|
|
367
|
-
JSON.stringify({
|
|
368
|
-
id: "stream-response",
|
|
369
|
-
model: "base-model",
|
|
370
|
-
choices: [{ index: 0, delta: { content: "hello" } }],
|
|
371
|
-
}),
|
|
372
|
-
),
|
|
373
|
-
);
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
const request = parseChatCompletionsRequest(
|
|
378
|
-
encoder.encode(
|
|
379
|
-
JSON.stringify({
|
|
380
|
-
model: "virtual-model",
|
|
381
|
-
messages: [{ role: "user", content: "hello" }],
|
|
382
|
-
}),
|
|
383
|
-
),
|
|
384
|
-
);
|
|
385
|
-
const first = await ctx.invoke("smart-model", request);
|
|
386
|
-
const chunks: Program[] = [];
|
|
387
|
-
for await (const chunk of ctx.invokeStream("base-model", request)) {
|
|
388
|
-
chunks.push(chunk);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
expect(calls).toEqual([
|
|
392
|
-
{ model: "smart-model", streaming: false },
|
|
393
|
-
{ model: "base-model", streaming: true },
|
|
394
|
-
]);
|
|
395
|
-
expect(contentText(first)).toBe("model answer");
|
|
396
|
-
expect(
|
|
397
|
-
decoder.decode(emitChatCompletionsStreamChunk(chunks[0]!)),
|
|
398
|
-
).toContain("hello");
|
|
399
|
-
expect(getModel(request)).toBe("virtual-model");
|
|
400
|
-
|
|
401
|
-
const customExecutor = executorFromExecute(async () =>
|
|
402
|
-
appendAssistantMessage({ code: [], buffers: [] }, "custom answer"),
|
|
403
|
-
);
|
|
404
|
-
const custom = await ctx.invoke(customExecutor, request);
|
|
405
|
-
expect(contentText(custom)).toBe("custom answer");
|
|
406
|
-
|
|
407
|
-
const twoPass = createTwoPassExecutor({
|
|
408
|
-
draft: customExecutor,
|
|
409
|
-
final: customExecutor,
|
|
410
|
-
});
|
|
411
|
-
expect(contentText(await twoPass.execute(request, ctx))).toBe(
|
|
412
|
-
"custom answer",
|
|
413
|
-
);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it("can skip the draft pass and limits the final context length", async () => {
|
|
417
|
-
const request = parseChatCompletionsRequest(
|
|
418
|
-
encoder.encode(
|
|
419
|
-
JSON.stringify({
|
|
420
|
-
model: "virtual-model",
|
|
421
|
-
messages: [{ role: "user", content: "request" }],
|
|
422
|
-
}),
|
|
423
|
-
),
|
|
424
|
-
);
|
|
425
|
-
const seen: Program[] = [];
|
|
426
|
-
const ctx = buildExecutorContext({
|
|
427
|
-
async invoke(_executor, program) {
|
|
428
|
-
seen.push(program);
|
|
429
|
-
return appendAssistantMessage(
|
|
430
|
-
{ code: [], buffers: [] },
|
|
431
|
-
seen.length === 1 ? "a very long draft" : "final",
|
|
432
|
-
);
|
|
433
|
-
},
|
|
434
|
-
async *invokeStream() {
|
|
435
|
-
throw new Error("streaming path is not used in this test");
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
const executor = createTwoPassExecutor({
|
|
440
|
-
draft: "smart-model",
|
|
441
|
-
final: "base-model",
|
|
442
|
-
maxTotalContextLength: 10,
|
|
443
|
-
resolveDraftSettings: () => ({ systemPrompt: "draft" }),
|
|
444
|
-
});
|
|
445
|
-
await executor.execute(request, ctx);
|
|
446
|
-
|
|
447
|
-
const finalRequest = JSON.parse(
|
|
448
|
-
decoder.decode(emitChatCompletionsRequest(seen[1]!)),
|
|
449
|
-
);
|
|
450
|
-
expect(finalRequest.messages.at(-1)?.content).toBe("a v");
|
|
451
|
-
|
|
452
|
-
let receivedRequest: Program | undefined;
|
|
453
|
-
let receivedContext: ExecutorContext | undefined;
|
|
454
|
-
const directFinal = createTwoPassExecutor({
|
|
455
|
-
draft: "unused",
|
|
456
|
-
final: "base-model",
|
|
457
|
-
resolveDraftSettings: (incomingRequest, incomingContext) => {
|
|
458
|
-
receivedRequest = incomingRequest;
|
|
459
|
-
receivedContext = incomingContext;
|
|
460
|
-
return null;
|
|
461
|
-
},
|
|
462
|
-
});
|
|
463
|
-
await directFinal.execute(request, ctx);
|
|
464
|
-
expect(getModel(seen.at(-1)!)).toBe("base-model");
|
|
465
|
-
expect(receivedRequest).toBe(request);
|
|
466
|
-
expect(receivedContext).toBe(ctx);
|
|
467
|
-
|
|
468
|
-
const draftOnly = createTwoPassExecutor({
|
|
469
|
-
draft: "smart-model",
|
|
470
|
-
final: "unused",
|
|
471
|
-
resolveFinalSettings: () => null,
|
|
472
|
-
});
|
|
473
|
-
await draftOnly.execute(request, ctx);
|
|
474
|
-
expect(getModel(seen.at(-1)!)).toBe("smart-model");
|
|
475
|
-
});
|
|
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
|
-
|
|
518
246
|
it("writes reasoning helpers to a sink", async () => {
|
|
519
247
|
const emitted: string[] = [];
|
|
520
248
|
const sink: OutputSink = {
|
|
521
249
|
write(chunk) {
|
|
522
|
-
emitted.push(
|
|
250
|
+
emitted.push(
|
|
251
|
+
decoder.decode(emitProviderStreamChunk("chat-completions", chunk)),
|
|
252
|
+
);
|
|
523
253
|
},
|
|
524
254
|
close() {
|
|
525
255
|
emitted.push("[DONE]");
|
|
526
256
|
},
|
|
527
257
|
};
|
|
528
258
|
|
|
529
|
-
await
|
|
259
|
+
await writeThinking(sink, "thinking");
|
|
530
260
|
|
|
531
261
|
expect(JSON.parse(emitted[0]!).choices[0].delta.reasoning_content).toBe(
|
|
532
262
|
"thinking",
|
|
@@ -551,7 +281,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
551
281
|
);
|
|
552
282
|
|
|
553
283
|
expect(calls).toBe(2);
|
|
554
|
-
expect(
|
|
284
|
+
expect(extractContentText(result)).toBe("ok");
|
|
555
285
|
});
|
|
556
286
|
|
|
557
287
|
it("falls back to the next executor after failure", async () => {
|
|
@@ -569,7 +299,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
569
299
|
buildExecutorContext(),
|
|
570
300
|
);
|
|
571
301
|
|
|
572
|
-
expect(
|
|
302
|
+
expect(extractContentText(result)).toBe("fallback");
|
|
573
303
|
});
|
|
574
304
|
|
|
575
305
|
it("does not retry a stream after exposing a partial chunk", async () => {
|
|
@@ -630,7 +360,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
630
360
|
),
|
|
631
361
|
),
|
|
632
362
|
satisfied(review) {
|
|
633
|
-
return
|
|
363
|
+
return extractContentText(review) === "pass";
|
|
634
364
|
},
|
|
635
365
|
refine(request) {
|
|
636
366
|
return request;
|
|
@@ -643,7 +373,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
643
373
|
);
|
|
644
374
|
|
|
645
375
|
expect(attempts).toBe(2);
|
|
646
|
-
expect(
|
|
376
|
+
expect(extractContentText(result)).toBe("answer 2");
|
|
647
377
|
});
|
|
648
378
|
|
|
649
379
|
it("streams draft and review passes through each goal iteration", async () => {
|
|
@@ -663,7 +393,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
663
393
|
);
|
|
664
394
|
}),
|
|
665
395
|
satisfied(review) {
|
|
666
|
-
return
|
|
396
|
+
return extractContentText(review) === "pass";
|
|
667
397
|
},
|
|
668
398
|
refine(request) {
|
|
669
399
|
return request;
|
|
@@ -678,7 +408,7 @@ describe("@neutrome/lilsdk", () => {
|
|
|
678
408
|
chunks.push(chunk);
|
|
679
409
|
}
|
|
680
410
|
|
|
681
|
-
expect(chunks.map(
|
|
411
|
+
expect(chunks.map(extractContentText).filter(Boolean)).toEqual([
|
|
682
412
|
"answer 1",
|
|
683
413
|
"retry",
|
|
684
414
|
"answer 2",
|
|
@@ -745,14 +475,14 @@ describe("@neutrome/lilsdk", () => {
|
|
|
745
475
|
let reviewedAnswer = "";
|
|
746
476
|
const executor = createGoalExecutor({
|
|
747
477
|
draft: streamingExecutor(async function* () {
|
|
748
|
-
yield*
|
|
478
|
+
yield* delta.textResponse("answer");
|
|
749
479
|
}),
|
|
750
480
|
review: streamingExecutor(async function* (request) {
|
|
751
|
-
reviewedAnswer =
|
|
752
|
-
yield*
|
|
481
|
+
reviewedAnswer = extractContentText(request);
|
|
482
|
+
yield* delta.textResponse("pass");
|
|
753
483
|
}),
|
|
754
484
|
satisfied(review) {
|
|
755
|
-
return
|
|
485
|
+
return extractContentText(review) === "pass";
|
|
756
486
|
},
|
|
757
487
|
refine(request) {
|
|
758
488
|
return request;
|