@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.
@@ -6,9 +6,8 @@ import {
6
6
  type Instruction,
7
7
  type Program,
8
8
  } from "@neutrome/lil-engine";
9
- import { createTargetExecutor } from "../src/managed/target-executor.ts";
10
9
  import { connectTools, ToolArgumentsError } from "../src/tools.ts";
11
- import type { ExecutionTarget, ExecutorContext, Tool } from "../src/types.ts";
10
+ import type { ExecutorContext, Tool } from "../src/types.ts";
12
11
 
13
12
  function buildToolCallResponse(
14
13
  calls: Array<{ id: string; name: string; args: string }>,
@@ -65,12 +64,47 @@ const calculatorTool: Tool = {
65
64
  };
66
65
 
67
66
  describe("connectTools", () => {
67
+ it("passes the executor cache to connected tools", async () => {
68
+ let callCount = 0;
69
+ const cache = {
70
+ async get(key: string) {
71
+ return key === "tool-key" ? "cached-value" : null;
72
+ },
73
+ async put() {},
74
+ async delete() {},
75
+ };
76
+ const tool: Tool = {
77
+ name: "cache-reader",
78
+ description: "Read a cached value",
79
+ schema: { type: "object", properties: {} },
80
+ async execute(_args, ctx) {
81
+ expect(ctx.cache).toBe(cache);
82
+ return (await ctx.cache.get("tool-key")) ?? "missing";
83
+ },
84
+ };
85
+ const executor = connectTools([tool], "test-model");
86
+ const ctx = buildCtx({
87
+ async invoke() {
88
+ callCount += 1;
89
+ return callCount === 1
90
+ ? buildToolCallResponse([
91
+ { id: "call_1", name: tool.name, args: "{}" },
92
+ ])
93
+ : buildTextResponse("done");
94
+ },
95
+ async *invokeStream() {
96
+ return;
97
+ },
98
+ });
99
+ ctx.cache = cache;
100
+
101
+ await executor.execute(createProgram(), ctx);
102
+
103
+ expect(callCount).toBe(2);
104
+ });
105
+
68
106
  it("passes through when LLM returns no tool calls", async () => {
69
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
70
- const executor = connectTools(
71
- [calculatorTool],
72
- createTargetExecutor(target),
73
- );
107
+ const executor = connectTools([calculatorTool], "test-model");
74
108
 
75
109
  const request = createProgram({
76
110
  code: [
@@ -81,17 +115,14 @@ describe("connectTools", () => {
81
115
  ],
82
116
  });
83
117
 
84
- const ctx = buildCtx(
85
- {
86
- async invoke() {
87
- return buildTextResponse("Hello!");
88
- },
89
- async *invokeStream() {
90
- yield buildTextResponse("Hello!");
91
- },
118
+ const ctx = buildCtx({
119
+ async invoke() {
120
+ return buildTextResponse("Hello!");
92
121
  },
93
- target,
94
- );
122
+ async *invokeStream() {
123
+ yield buildTextResponse("Hello!");
124
+ },
125
+ });
95
126
  const result = await executor.execute(request, ctx);
96
127
 
97
128
  const hasText = result.code.some(
@@ -104,11 +135,7 @@ describe("connectTools", () => {
104
135
  });
105
136
 
106
137
  it("executes connected tool and loops until text response", async () => {
107
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
108
- const executor = connectTools(
109
- [calculatorTool],
110
- createTargetExecutor(target),
111
- );
138
+ const executor = connectTools([calculatorTool], "test-model");
112
139
 
113
140
  let callCount = 0;
114
141
  const request = createProgram({
@@ -120,23 +147,20 @@ describe("connectTools", () => {
120
147
  ],
121
148
  });
122
149
 
123
- const ctx = buildCtx(
124
- {
125
- async invoke() {
126
- callCount += 1;
127
- if (callCount === 1) {
128
- return buildToolCallResponse([
129
- { id: "call_1", name: "calculator", args: '{"expr":"2+2"}' },
130
- ]);
131
- }
132
- return buildTextResponse("The answer is 4");
133
- },
134
- async *invokeStream() {
135
- yield buildTextResponse("The answer is 4");
136
- },
150
+ const ctx = buildCtx({
151
+ async invoke() {
152
+ callCount += 1;
153
+ if (callCount === 1) {
154
+ return buildToolCallResponse([
155
+ { id: "call_1", name: "calculator", args: '{"expr":"2+2"}' },
156
+ ]);
157
+ }
158
+ return buildTextResponse("The answer is 4");
137
159
  },
138
- target,
139
- );
160
+ async *invokeStream() {
161
+ yield buildTextResponse("The answer is 4");
162
+ },
163
+ });
140
164
  const result = await executor.execute(request, ctx);
141
165
 
142
166
  expect(callCount).toBe(2);
@@ -150,24 +174,17 @@ describe("connectTools", () => {
150
174
  });
151
175
 
152
176
  it("rejects malformed connected-tool arguments", async () => {
153
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
154
- const executor = connectTools(
155
- [calculatorTool],
156
- createTargetExecutor(target),
157
- );
158
- const ctx = buildCtx(
159
- {
160
- async invoke() {
161
- return buildToolCallResponse([
162
- { id: "call_1", name: "calculator", args: "{not json}" },
163
- ]);
164
- },
165
- async *invokeStream() {
166
- return;
167
- },
177
+ const executor = connectTools([calculatorTool], "test-model");
178
+ const ctx = buildCtx({
179
+ async invoke() {
180
+ return buildToolCallResponse([
181
+ { id: "call_1", name: "calculator", args: "{not json}" },
182
+ ]);
168
183
  },
169
- target,
170
- );
184
+ async *invokeStream() {
185
+ return;
186
+ },
187
+ });
171
188
 
172
189
  await expect(executor.execute(createProgram(), ctx)).rejects.toBeInstanceOf(
173
190
  ToolArgumentsError,
@@ -186,30 +203,25 @@ describe("connectTools", () => {
186
203
  return `value_of_${key}`;
187
204
  },
188
205
  };
189
-
190
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
191
- const executor = connectTools([multiTool], createTargetExecutor(target));
206
+ const executor = connectTools([multiTool], "test-model");
192
207
 
193
208
  let callCount = 0;
194
209
  const request = createProgram();
195
- const ctx = buildCtx(
196
- {
197
- async invoke() {
198
- callCount += 1;
199
- if (callCount === 1) {
200
- return buildToolCallResponse([
201
- { id: "call_a", name: "lookup", args: '{"key":"alpha"}' },
202
- { id: "call_b", name: "lookup", args: '{"key":"beta"}' },
203
- ]);
204
- }
205
- return buildTextResponse("Done");
206
- },
207
- async *invokeStream() {
208
- yield buildTextResponse("Done");
209
- },
210
+ const ctx = buildCtx({
211
+ async invoke() {
212
+ callCount += 1;
213
+ if (callCount === 1) {
214
+ return buildToolCallResponse([
215
+ { id: "call_a", name: "lookup", args: '{"key":"alpha"}' },
216
+ { id: "call_b", name: "lookup", args: '{"key":"beta"}' },
217
+ ]);
218
+ }
219
+ return buildTextResponse("Done");
210
220
  },
211
- target,
212
- );
221
+ async *invokeStream() {
222
+ yield buildTextResponse("Done");
223
+ },
224
+ });
213
225
  const result = await executor.execute(request, ctx);
214
226
 
215
227
  expect(callCount).toBe(2);
@@ -218,27 +230,20 @@ describe("connectTools", () => {
218
230
  });
219
231
 
220
232
  it("returns immediately when outer (non-connected) tool calls are present", async () => {
221
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
222
- const executor = connectTools(
223
- [calculatorTool],
224
- createTargetExecutor(target),
225
- );
233
+ const executor = connectTools([calculatorTool], "test-model");
226
234
 
227
235
  const request = createProgram();
228
- const ctx = buildCtx(
229
- {
230
- async invoke() {
231
- return buildToolCallResponse([
232
- { id: "call_1", name: "calculator", args: '{"expr":"1+1"}' },
233
- { id: "call_2", name: "external_tool", args: "{}" },
234
- ]);
235
- },
236
- async *invokeStream() {
237
- return;
238
- },
236
+ const ctx = buildCtx({
237
+ async invoke() {
238
+ return buildToolCallResponse([
239
+ { id: "call_1", name: "calculator", args: '{"expr":"1+1"}' },
240
+ { id: "call_2", name: "external_tool", args: "{}" },
241
+ ]);
239
242
  },
240
- target,
241
- );
243
+ async *invokeStream() {
244
+ return;
245
+ },
246
+ });
242
247
  let executions = 0;
243
248
  const tool: Tool = {
244
249
  ...calculatorTool,
@@ -247,7 +252,7 @@ describe("connectTools", () => {
247
252
  return calculatorTool.execute(args, ctx);
248
253
  },
249
254
  };
250
- const guardedExecutor = connectTools([tool], createTargetExecutor(target));
255
+ const guardedExecutor = connectTools([tool], "test-model");
251
256
  const result = await guardedExecutor.execute(request, ctx);
252
257
 
253
258
  const calls = callData(result);
@@ -257,33 +262,27 @@ describe("connectTools", () => {
257
262
  });
258
263
 
259
264
  it("respects maxIterations", async () => {
260
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
261
- const executor = connectTools(
262
- [calculatorTool],
263
- createTargetExecutor(target),
264
- { maxIterations: 2 },
265
- );
265
+ const executor = connectTools([calculatorTool], "test-model", {
266
+ maxIterations: 2,
267
+ });
266
268
 
267
269
  let callCount = 0;
268
270
  const request = createProgram();
269
- const ctx = buildCtx(
270
- {
271
- async invoke() {
272
- callCount += 1;
273
- return buildToolCallResponse([
274
- {
275
- id: `call_${callCount}`,
276
- name: "calculator",
277
- args: '{"expr":"1"}',
278
- },
279
- ]);
280
- },
281
- async *invokeStream() {
282
- yield buildTextResponse("final");
283
- },
271
+ const ctx = buildCtx({
272
+ async invoke() {
273
+ callCount += 1;
274
+ return buildToolCallResponse([
275
+ {
276
+ id: `call_${callCount}`,
277
+ name: "calculator",
278
+ args: '{"expr":"1"}',
279
+ },
280
+ ]);
284
281
  },
285
- target,
286
- );
282
+ async *invokeStream() {
283
+ yield buildTextResponse("final");
284
+ },
285
+ });
287
286
  await executor.execute(request, ctx);
288
287
 
289
288
  // 2 iterations in the loop + 1 final invoke
@@ -291,50 +290,43 @@ describe("connectTools", () => {
291
290
  });
292
291
 
293
292
  it("streams final response after tool loop completes", async () => {
294
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
295
- const executor = connectTools(
296
- [calculatorTool],
297
- createTargetExecutor(target),
298
- );
293
+ const executor = connectTools([calculatorTool], "test-model");
299
294
 
300
295
  let callCount = 0;
301
296
  const request = createProgram();
302
- const ctx = buildCtx(
303
- {
304
- async invoke() {
305
- callCount += 1;
306
- if (callCount === 1) {
307
- return buildToolCallResponse([
308
- { id: "call_1", name: "calculator", args: '{"expr":"3+3"}' },
309
- ]);
310
- }
311
- return buildTextResponse("6");
312
- },
313
- async *invokeStream() {
314
- yield createProgram({
315
- code: [{ opcode: Opcode.STREAM_START, value: { kind: "none" } }],
316
- });
317
- yield createProgram({
318
- code: [
319
- {
320
- opcode: Opcode.STREAM_DELTA,
321
- value: { kind: "string", value: "6" },
322
- },
323
- ],
324
- });
325
- yield createProgram({
326
- code: [
327
- {
328
- opcode: Opcode.RESP_DONE,
329
- value: { kind: "string", value: "stop" },
330
- },
331
- { opcode: Opcode.STREAM_END, value: { kind: "none" } },
332
- ],
333
- });
334
- },
297
+ const ctx = buildCtx({
298
+ async invoke() {
299
+ callCount += 1;
300
+ if (callCount === 1) {
301
+ return buildToolCallResponse([
302
+ { id: "call_1", name: "calculator", args: '{"expr":"3+3"}' },
303
+ ]);
304
+ }
305
+ return buildTextResponse("6");
335
306
  },
336
- target,
337
- );
307
+ async *invokeStream() {
308
+ yield createProgram({
309
+ code: [{ opcode: Opcode.STREAM_START, value: { kind: "none" } }],
310
+ });
311
+ yield createProgram({
312
+ code: [
313
+ {
314
+ opcode: Opcode.STREAM_DELTA,
315
+ value: { kind: "string", value: "6" },
316
+ },
317
+ ],
318
+ });
319
+ yield createProgram({
320
+ code: [
321
+ {
322
+ opcode: Opcode.RESP_DONE,
323
+ value: { kind: "string", value: "stop" },
324
+ },
325
+ { opcode: Opcode.STREAM_END, value: { kind: "none" } },
326
+ ],
327
+ });
328
+ },
329
+ });
338
330
 
339
331
  const chunks: Program[] = [];
340
332
  for await (const chunk of executor.stream(request, ctx)) {
@@ -349,7 +341,6 @@ describe("connectTools", () => {
349
341
  });
350
342
 
351
343
  it("assembles split stream tool deltas before executing a connected tool", async () => {
352
- const target: ExecutionTarget = { kind: "provider", model: "test-model" };
353
344
  const calls: unknown[] = [];
354
345
  const tool: Tool = {
355
346
  ...calculatorTool,
@@ -358,47 +349,44 @@ describe("connectTools", () => {
358
349
  return "4";
359
350
  },
360
351
  };
361
- const executor = connectTools([tool], createTargetExecutor(target));
352
+ const executor = connectTools([tool], "test-model");
362
353
  let invocation = 0;
363
- const ctx = buildCtx(
364
- {
365
- async invoke() {
366
- return buildTextResponse("unused");
367
- },
368
- async *invokeStream() {
369
- invocation += 1;
370
- if (invocation === 1) {
371
- yield streamToolDelta({ index: 0, id: "call_1" });
372
- yield streamToolDelta({ index: 0, name: "calculator" });
373
- yield streamToolDelta({ index: 0, arguments: '{"expr":"2+2"}' });
374
- yield createProgram({
375
- code: [
376
- {
377
- opcode: Opcode.RESP_DONE,
378
- value: { kind: "string", value: "tool_calls" },
379
- },
380
- { opcode: Opcode.STREAM_END, value: { kind: "none" } },
381
- ],
382
- });
383
- return;
384
- }
354
+ const ctx = buildCtx({
355
+ async invoke() {
356
+ return buildTextResponse("unused");
357
+ },
358
+ async *invokeStream() {
359
+ invocation += 1;
360
+ if (invocation === 1) {
361
+ yield streamToolDelta({ index: 0, id: "call_1" });
362
+ yield streamToolDelta({ index: 0, name: "calculator" });
363
+ yield streamToolDelta({ index: 0, arguments: '{"expr":"2+2"}' });
385
364
  yield createProgram({
386
365
  code: [
387
- {
388
- opcode: Opcode.STREAM_DELTA,
389
- value: { kind: "string", value: "4" },
390
- },
391
366
  {
392
367
  opcode: Opcode.RESP_DONE,
393
- value: { kind: "string", value: "stop" },
368
+ value: { kind: "string", value: "tool_calls" },
394
369
  },
395
370
  { opcode: Opcode.STREAM_END, value: { kind: "none" } },
396
371
  ],
397
372
  });
398
- },
373
+ return;
374
+ }
375
+ yield createProgram({
376
+ code: [
377
+ {
378
+ opcode: Opcode.STREAM_DELTA,
379
+ value: { kind: "string", value: "4" },
380
+ },
381
+ {
382
+ opcode: Opcode.RESP_DONE,
383
+ value: { kind: "string", value: "stop" },
384
+ },
385
+ { opcode: Opcode.STREAM_END, value: { kind: "none" } },
386
+ ],
387
+ });
399
388
  },
400
- target,
401
- );
389
+ });
402
390
 
403
391
  const chunks: Program[] = [];
404
392
  for await (const chunk of executor.stream(createProgram(), ctx))
@@ -434,19 +422,27 @@ function streamToolDelta(delta: Record<string, unknown>): Program {
434
422
 
435
423
  function buildCtx(
436
424
  impl: Pick<ExecutorContext, "invoke" | "invokeStream">,
437
- target: ExecutionTarget,
438
425
  ): ExecutorContext {
439
426
  const signal = new AbortController().signal;
440
427
  return {
428
+ cache: testCache,
441
429
  requestId: "req_test",
442
430
  executionId: "exec_test",
443
- async invoke(request: Program, options) {
444
- return impl.invoke(request, { target: options?.target ?? target });
431
+ async invoke(executor, request) {
432
+ return impl.invoke(executor, request);
445
433
  },
446
- async *invokeStream(request: Program, options) {
447
- yield* impl.invokeStream(request, { target: options?.target ?? target });
434
+ async *invokeStream(executor, request) {
435
+ yield* impl.invokeStream(executor, request);
448
436
  },
449
437
  observe: () => {},
450
438
  signal,
451
439
  };
452
440
  }
441
+
442
+ const testCache: ExecutorContext["cache"] = {
443
+ async get() {
444
+ return null;
445
+ },
446
+ async put() {},
447
+ async delete() {},
448
+ };
@@ -1,12 +0,0 @@
1
- import type { ExecutionTarget, Executor } from "../types.ts";
2
-
3
- export function createTargetExecutor(target: ExecutionTarget): Executor {
4
- return {
5
- execute(request, context) {
6
- return context.invoke(request, { target });
7
- },
8
- stream(request, context) {
9
- return context.invokeStream(request, { target });
10
- },
11
- };
12
- }