@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.
@@ -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 type { Executor, ExecutorContext, InvokeOptions } from "../types.ts";
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
- draftModel: ModelExecutor;
41
- finalModel: ModelExecutor;
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 invokeExecutor(ctx, draftRequest, options.draftModel);
62
+ return ctx.invoke(options.draft, draftRequest);
88
63
  }
89
64
 
90
- const draftResponse = await invokeExecutor(
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* streamExecutor(ctx, draftRequest, options.draftModel);
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 streamExecutor(
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 (toolMode || ctx.signal.aborted) return;
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 invokeExecutor(
182
- ctx,
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* streamExecutor(
196
- ctx,
167
+ yield* ctx.invokeStream(
168
+ options.final,
197
169
  buildFinalRequest(options, request, settings, draft),
198
- options.finalModel,
199
170
  );
200
171
  }
@@ -1,6 +1,7 @@
1
1
  export type { ObservedExecution, StreamObservationHooks } from "../observe.ts";
2
2
 
3
3
  export { observeExecutionStream } from "../observe.ts";
4
+ export { completeStream, streamStage } from "./stages.ts";
4
5
  export {
5
6
  streamReasoningDelta,
6
7
  streamTextResponse,
@@ -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 { type Executor, type ExecutorContext, type Tool } from "./types.ts";
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: Executor,
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 inner.execute(state.request, ctx);
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 inner.execute(state.request, ctx);
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) => inner.stream(req, ctx),
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 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,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
- streamExecutor,
37
- } from "../src/managed/twoPassExecutor.ts";
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
- draftModel: "smart-model",
220
- finalModel: "base-model",
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
- draftModel: "smart-model",
282
- finalModel: "base-model",
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("invokes and streams string or custom executors", async () => {
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 invokeExecutor(ctx, request, "smart-model");
385
+ const first = await ctx.invoke("smart-model", request);
331
386
  const chunks: Program[] = [];
332
- for await (const chunk of streamExecutor(ctx, request, "base-model")) {
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 invokeExecutor(ctx, request, customExecutor);
404
+ const custom = await ctx.invoke(customExecutor, request);
350
405
  expect(contentText(custom)).toBe("custom answer");
351
406
 
352
407
  const twoPass = createTwoPassExecutor({
353
- draftModel: customExecutor,
354
- finalModel: customExecutor,
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
- draftModel: "smart-model",
386
- finalModel: "base-model",
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
- draftModel: "unused",
401
- finalModel: "base-model",
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
- draftModel: "smart-model",
415
- finalModel: "unused",
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(program) {
625
- return program;
775
+ async invoke(_executor, request) {
776
+ return request;
626
777
  },
627
- async *invokeStream(program) {
628
- yield program;
778
+ async *invokeStream(_executor, request) {
779
+ yield request;
629
780
  },
630
781
  },
631
782
  ): ExecutorContext {
632
- return {
783
+ const context: ExecutorContext = {
784
+ cache: testCache,
633
785
  requestId: "req_test",
634
786
  executionId: "exec_test",
635
- invoke: impl.invoke,
636
- invokeStream: impl.invokeStream,
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,