@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.
- package/package.json +2 -2
- package/src/index.ts +3 -2
- package/src/loops/index.ts +24 -18
- package/src/managed/capabilities.ts +215 -0
- package/src/managed/index.ts +4 -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 +149 -32
- package/test/tools.test.ts +190 -194
- package/src/managed/target-executor.ts +0 -12
package/test/tools.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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],
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
{
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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],
|
|
352
|
+
const executor = connectTools([tool], "test-model");
|
|
362
353
|
let invocation = 0;
|
|
363
|
-
const ctx = buildCtx(
|
|
364
|
-
{
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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: "
|
|
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
|
-
|
|
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(
|
|
444
|
-
return impl.invoke(
|
|
431
|
+
async invoke(executor, request) {
|
|
432
|
+
return impl.invoke(executor, request);
|
|
445
433
|
},
|
|
446
|
-
async *invokeStream(
|
|
447
|
-
yield* impl.invokeStream(
|
|
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
|
-
}
|