@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.
@@ -1,16 +1,17 @@
1
1
  import {
2
- contentText,
2
+ appendAssistantMessage,
3
+ appendToolInteraction,
3
4
  createProgram,
4
- deltaText,
5
- emitChatCompletionsRequest,
6
- emitChatCompletionsStreamChunk,
7
- getModel,
8
- parseChatCompletionsRequest,
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 = parseChatCompletionsRequest(
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(programAttachments(innerRequest!)).toHaveLength(0);
112
+ expect(extractAttachments(innerRequest!)).toHaveLength(0);
95
113
  expect(
96
- viewProgram(innerRequest!).messages.at(-1)?.toolResult?.text,
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 parseChatCompletionsStreamChunk(
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 parseChatCompletionsStreamChunk(
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 parseChatCompletionsStreamChunk(
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(decoder.decode(emitChatCompletionsStreamChunk(chunk))),
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 = parseChatCompletionsRequest(
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(emitChatCompletionsRequest(updated)),
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(decoder.decode(emitChatCompletionsStreamChunk(chunk)));
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 writeReasoning(sink, "thinking");
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(contentText(result)).toBe("ok");
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(contentText(result)).toBe("fallback");
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 contentText(review) === "pass";
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(contentText(result)).toBe("answer 2");
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 contentText(review) === "pass";
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(contentText).filter(Boolean)).toEqual([
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* streamTextResponse("answer");
478
+ yield* delta.textResponse("answer");
749
479
  }),
750
480
  review: streamingExecutor(async function* (request) {
751
- reviewedAnswer = contentText(request);
752
- yield* streamTextResponse("pass");
481
+ reviewedAnswer = extractContentText(request);
482
+ yield* delta.textResponse("pass");
753
483
  }),
754
484
  satisfied(review) {
755
- return contentText(review) === "pass";
485
+ return extractContentText(review) === "pass";
756
486
  },
757
487
  refine(request) {
758
488
  return request;