@neutrome/lilsdk-ts 0.2.4 → 0.3.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 +7 -4
- package/package.json +2 -2
- package/src/index.ts +3 -3
- package/src/loops/index.ts +50 -19
- package/src/managed/index.ts +1 -1
- package/src/managed/target-executor.ts +12 -0
- package/src/managed/twoPassExecutor.ts +18 -6
- package/src/observe.ts +2 -1
- package/src/output.ts +8 -2
- package/src/primitives/index.ts +45 -11
- package/src/stream/index.ts +5 -1
- package/src/synthetic/index.ts +22 -39
- package/src/tools-support.ts +108 -0
- package/src/tools.ts +144 -228
- package/src/types.ts +17 -7
- package/test/lilsdk-ts.test.ts +266 -102
- package/test/tools.test.ts +287 -94
- package/src/managed/wrap.ts +0 -17
package/test/lilsdk-ts.test.ts
CHANGED
|
@@ -9,10 +9,21 @@ import {
|
|
|
9
9
|
type Program,
|
|
10
10
|
} from "@neutrome/lil-engine";
|
|
11
11
|
import { describe, expect, it } from "vitest";
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
all,
|
|
14
|
+
any,
|
|
15
|
+
find,
|
|
16
|
+
has,
|
|
17
|
+
indexOf,
|
|
18
|
+
map,
|
|
19
|
+
reduce,
|
|
20
|
+
} from "../src/primitives/index.ts";
|
|
21
|
+
import {
|
|
22
|
+
appendAssistantMessage,
|
|
23
|
+
appendToolInteraction,
|
|
24
|
+
} from "../src/synthetic/index.ts";
|
|
14
25
|
import { observeExecutionStream, writeReasoning } from "../src/stream/index.ts";
|
|
15
|
-
import { fallback,
|
|
26
|
+
import { fallback, retry, runGoal } from "../src/loops/index.ts";
|
|
16
27
|
import {
|
|
17
28
|
appendInternalDraft,
|
|
18
29
|
createTwoPassExecutor,
|
|
@@ -20,18 +31,17 @@ import {
|
|
|
20
31
|
INTERNAL_DRAFT_TOOL_NAME,
|
|
21
32
|
streamModel,
|
|
22
33
|
} from "../src/managed/twoPassExecutor.ts";
|
|
23
|
-
import type {
|
|
24
|
-
Executor,
|
|
25
|
-
ExecutorContext,
|
|
26
|
-
OutputSink,
|
|
27
|
-
} from "../src/types.ts";
|
|
34
|
+
import type { Executor, ExecutorContext, OutputSink } from "../src/types.ts";
|
|
28
35
|
|
|
29
36
|
const encoder = new TextEncoder();
|
|
30
37
|
const decoder = new TextDecoder();
|
|
31
38
|
|
|
32
39
|
describe("@neutrome/lilsdk-ts", () => {
|
|
33
40
|
it("provides clone-safe structural primitives", () => {
|
|
34
|
-
const needle = {
|
|
41
|
+
const needle = {
|
|
42
|
+
opcode: Opcode.MSG_START,
|
|
43
|
+
value: { kind: "none" as const },
|
|
44
|
+
};
|
|
35
45
|
const program: Program = {
|
|
36
46
|
code: [
|
|
37
47
|
needle,
|
|
@@ -44,8 +54,12 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
44
54
|
expect(has(program, needle)).toBe(true);
|
|
45
55
|
expect(find(program, needle)).toEqual(needle);
|
|
46
56
|
expect(reduce(program, (count) => count + 1, 0)).toBe(2);
|
|
47
|
-
expect(
|
|
48
|
-
|
|
57
|
+
expect(
|
|
58
|
+
all(program, (instruction) => instruction.opcode !== Opcode.MSG_END),
|
|
59
|
+
).toBe(true);
|
|
60
|
+
expect(
|
|
61
|
+
any(program, (instruction) => instruction.opcode === Opcode.TXT_CHUNK),
|
|
62
|
+
).toBe(true);
|
|
49
63
|
|
|
50
64
|
const mapped = map(program, (instruction) => instruction);
|
|
51
65
|
mapped.buffers[0]![0] = 9;
|
|
@@ -53,36 +67,54 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
53
67
|
});
|
|
54
68
|
|
|
55
69
|
it("observes tool-call streams", async () => {
|
|
56
|
-
const observed = await observeExecutionStream(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
70
|
+
const observed = await observeExecutionStream(
|
|
71
|
+
(async function* () {
|
|
72
|
+
yield parseChatCompletionsStreamChunk(
|
|
73
|
+
encoder.encode(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
id: "tool-stream",
|
|
76
|
+
object: "chat.completion.chunk",
|
|
77
|
+
model: "smart",
|
|
78
|
+
choices: [{ index: 0, delta: { role: "assistant" } }],
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
yield parseChatCompletionsStreamChunk(
|
|
83
|
+
encoder.encode(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
id: "tool-stream",
|
|
86
|
+
object: "chat.completion.chunk",
|
|
87
|
+
model: "smart",
|
|
88
|
+
choices: [
|
|
89
|
+
{
|
|
90
|
+
index: 0,
|
|
91
|
+
delta: {
|
|
92
|
+
tool_calls: [
|
|
93
|
+
{
|
|
94
|
+
index: 0,
|
|
95
|
+
id: "call_lookup",
|
|
96
|
+
type: "function",
|
|
97
|
+
function: { name: "lookup", arguments: '{"q":"kyiv"}' },
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
yield parseChatCompletionsStreamChunk(
|
|
107
|
+
encoder.encode(
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
id: "tool-stream",
|
|
110
|
+
object: "chat.completion.chunk",
|
|
111
|
+
model: "smart",
|
|
112
|
+
choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }],
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
})(),
|
|
117
|
+
);
|
|
86
118
|
|
|
87
119
|
expect(observed.mode).toBe("tool");
|
|
88
120
|
if (observed.mode !== "tool") {
|
|
@@ -90,30 +122,44 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
const parsed = observed.chunks.map((chunk) =>
|
|
93
|
-
JSON.parse(decoder.decode(emitChatCompletionsStreamChunk(chunk)))
|
|
125
|
+
JSON.parse(decoder.decode(emitChatCompletionsStreamChunk(chunk))),
|
|
126
|
+
);
|
|
127
|
+
expect(parsed.some((chunk) => chunk.choices?.[0]?.delta?.tool_calls)).toBe(
|
|
128
|
+
true,
|
|
94
129
|
);
|
|
95
|
-
expect(parsed.some((chunk) => chunk.choices?.[0]?.delta?.tool_calls)).toBe(true);
|
|
96
130
|
});
|
|
97
131
|
|
|
98
132
|
it("appends internal drafts with fixed SDK metadata and empty args", () => {
|
|
99
|
-
const request = parseChatCompletionsRequest(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
const request = parseChatCompletionsRequest(
|
|
134
|
+
encoder.encode(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
model: "virtual-model",
|
|
137
|
+
messages: [{ role: "user", content: "hello" }],
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
103
141
|
|
|
104
142
|
const updated = appendInternalDraft(request, "draft answer");
|
|
105
|
-
const emitted = JSON.parse(
|
|
143
|
+
const emitted = JSON.parse(
|
|
144
|
+
decoder.decode(emitChatCompletionsRequest(updated)),
|
|
145
|
+
);
|
|
106
146
|
|
|
107
|
-
expect(emitted.messages[1].tool_calls[0].function.name).toBe(
|
|
147
|
+
expect(emitted.messages[1].tool_calls[0].function.name).toBe(
|
|
148
|
+
INTERNAL_DRAFT_TOOL_NAME,
|
|
149
|
+
);
|
|
108
150
|
expect(emitted.messages[1].tool_calls[0].function.arguments).toBe("{}");
|
|
109
151
|
expect(emitted.messages[2].content).toBe("draft answer");
|
|
110
152
|
});
|
|
111
153
|
|
|
112
154
|
it("appends generic synthetic tool interactions", () => {
|
|
113
|
-
const request = parseChatCompletionsRequest(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
155
|
+
const request = parseChatCompletionsRequest(
|
|
156
|
+
encoder.encode(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
model: "virtual-model",
|
|
159
|
+
messages: [{ role: "user", content: "hello" }],
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
117
163
|
|
|
118
164
|
const updated = appendToolInteraction(request, {
|
|
119
165
|
callId: "call_lookup",
|
|
@@ -121,32 +167,44 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
121
167
|
args: { id: "42" },
|
|
122
168
|
result: { status: "active" },
|
|
123
169
|
});
|
|
124
|
-
const emitted = JSON.parse(
|
|
170
|
+
const emitted = JSON.parse(
|
|
171
|
+
decoder.decode(emitChatCompletionsRequest(updated)),
|
|
172
|
+
);
|
|
125
173
|
|
|
126
174
|
expect(emitted.messages[1].tool_calls[0].function).toEqual({
|
|
127
175
|
name: "lookup",
|
|
128
|
-
arguments:
|
|
176
|
+
arguments: '{"id":"42"}',
|
|
129
177
|
});
|
|
130
178
|
expect(emitted.messages[2]).toEqual({
|
|
131
179
|
role: "tool",
|
|
132
180
|
tool_call_id: "call_lookup",
|
|
133
|
-
content:
|
|
181
|
+
content: '{"status":"active"}',
|
|
134
182
|
});
|
|
135
183
|
});
|
|
136
184
|
|
|
137
185
|
it("builds two-pass executors from model and prompt config", async () => {
|
|
138
|
-
const request = parseChatCompletionsRequest(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
186
|
+
const request = parseChatCompletionsRequest(
|
|
187
|
+
encoder.encode(
|
|
188
|
+
JSON.stringify({
|
|
189
|
+
model: "virtual-model",
|
|
190
|
+
messages: [{ role: "user", content: "hello" }],
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
);
|
|
142
194
|
const seen: Program[] = [];
|
|
143
195
|
const ctx = buildExecutorContext({
|
|
144
196
|
async invoke(program) {
|
|
145
197
|
seen.push(program);
|
|
146
198
|
if (seen.length === 1) {
|
|
147
|
-
return appendAssistantMessage(
|
|
199
|
+
return appendAssistantMessage(
|
|
200
|
+
{ code: [], buffers: [] },
|
|
201
|
+
"draft answer",
|
|
202
|
+
);
|
|
148
203
|
}
|
|
149
|
-
return appendAssistantMessage(
|
|
204
|
+
return appendAssistantMessage(
|
|
205
|
+
{ code: [], buffers: [] },
|
|
206
|
+
"final answer",
|
|
207
|
+
);
|
|
150
208
|
},
|
|
151
209
|
async *invokeStream() {
|
|
152
210
|
throw new Error("streaming path is not used in this test");
|
|
@@ -161,11 +219,19 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
161
219
|
|
|
162
220
|
const result = await executor.execute(request, ctx);
|
|
163
221
|
|
|
164
|
-
expect(seen.map((program) => getModel(program))).toEqual([
|
|
222
|
+
expect(seen.map((program) => getModel(program))).toEqual([
|
|
223
|
+
"smart-model",
|
|
224
|
+
"base-model",
|
|
225
|
+
]);
|
|
165
226
|
expect(contentText(result)).toBe("final answer");
|
|
166
227
|
|
|
167
|
-
const finalRequest = JSON.parse(
|
|
168
|
-
|
|
228
|
+
const finalRequest = JSON.parse(
|
|
229
|
+
decoder.decode(emitChatCompletionsRequest(seen[1]!)),
|
|
230
|
+
);
|
|
231
|
+
expect(finalRequest.messages[0]).toEqual({
|
|
232
|
+
role: "system",
|
|
233
|
+
content: "system prompt",
|
|
234
|
+
});
|
|
169
235
|
expect(finalRequest.messages[2].tool_calls[0].function).toEqual({
|
|
170
236
|
name: INTERNAL_DRAFT_TOOL_NAME,
|
|
171
237
|
arguments: "{}",
|
|
@@ -178,18 +244,25 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
178
244
|
});
|
|
179
245
|
|
|
180
246
|
it("replaces draft system prompt and moves existing system prompt to first user message", async () => {
|
|
181
|
-
const request = parseChatCompletionsRequest(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
247
|
+
const request = parseChatCompletionsRequest(
|
|
248
|
+
encoder.encode(
|
|
249
|
+
JSON.stringify({
|
|
250
|
+
model: "virtual-model",
|
|
251
|
+
messages: [
|
|
252
|
+
{ role: "system", content: "original system" },
|
|
253
|
+
{ role: "user", content: "hello" },
|
|
254
|
+
],
|
|
255
|
+
}),
|
|
256
|
+
),
|
|
257
|
+
);
|
|
188
258
|
const seen: Program[] = [];
|
|
189
259
|
const ctx = buildExecutorContext({
|
|
190
260
|
async invoke(program) {
|
|
191
261
|
seen.push(program);
|
|
192
|
-
return appendAssistantMessage(
|
|
262
|
+
return appendAssistantMessage(
|
|
263
|
+
{ code: [], buffers: [] },
|
|
264
|
+
seen.length === 1 ? "draft" : "final",
|
|
265
|
+
);
|
|
193
266
|
},
|
|
194
267
|
async *invokeStream() {
|
|
195
268
|
throw new Error("streaming path is not used in this test");
|
|
@@ -204,7 +277,9 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
204
277
|
|
|
205
278
|
await executor.execute(request, ctx);
|
|
206
279
|
|
|
207
|
-
const draftRequest = JSON.parse(
|
|
280
|
+
const draftRequest = JSON.parse(
|
|
281
|
+
decoder.decode(emitChatCompletionsRequest(seen[0]!)),
|
|
282
|
+
);
|
|
208
283
|
expect(draftRequest.messages).toEqual([
|
|
209
284
|
{ role: "system", content: "draft system" },
|
|
210
285
|
{ role: "user", content: "original system" },
|
|
@@ -217,31 +292,47 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
217
292
|
const ctx = buildExecutorContext({
|
|
218
293
|
async invoke(program) {
|
|
219
294
|
calls.push({ model: getModel(program) ?? "", streaming: false });
|
|
220
|
-
return appendAssistantMessage(
|
|
295
|
+
return appendAssistantMessage(
|
|
296
|
+
{ code: [], buffers: [] },
|
|
297
|
+
"model answer",
|
|
298
|
+
);
|
|
221
299
|
},
|
|
222
300
|
async *invokeStream(program) {
|
|
223
301
|
calls.push({ model: getModel(program) ?? "", streaming: true });
|
|
224
|
-
yield parseChatCompletionsStreamChunk(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
302
|
+
yield parseChatCompletionsStreamChunk(
|
|
303
|
+
encoder.encode(
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
id: "stream-response",
|
|
306
|
+
model: "base-model",
|
|
307
|
+
choices: [{ index: 0, delta: { content: "hello" } }],
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
);
|
|
229
311
|
},
|
|
230
312
|
});
|
|
231
313
|
|
|
232
|
-
const request = parseChatCompletionsRequest(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
314
|
+
const request = parseChatCompletionsRequest(
|
|
315
|
+
encoder.encode(
|
|
316
|
+
JSON.stringify({
|
|
317
|
+
model: "virtual-model",
|
|
318
|
+
messages: [{ role: "user", content: "hello" }],
|
|
319
|
+
}),
|
|
320
|
+
),
|
|
321
|
+
);
|
|
236
322
|
const first = await invokeModel(ctx, request, "smart-model");
|
|
237
323
|
const chunks: Program[] = [];
|
|
238
324
|
for await (const chunk of streamModel(ctx, request, "base-model")) {
|
|
239
325
|
chunks.push(chunk);
|
|
240
326
|
}
|
|
241
327
|
|
|
242
|
-
expect(calls).toEqual([
|
|
328
|
+
expect(calls).toEqual([
|
|
329
|
+
{ model: "smart-model", streaming: false },
|
|
330
|
+
{ model: "base-model", streaming: true },
|
|
331
|
+
]);
|
|
243
332
|
expect(contentText(first)).toBe("model answer");
|
|
244
|
-
expect(
|
|
333
|
+
expect(
|
|
334
|
+
decoder.decode(emitChatCompletionsStreamChunk(chunks[0]!)),
|
|
335
|
+
).toContain("hello");
|
|
245
336
|
expect(getModel(request)).toBe("virtual-model");
|
|
246
337
|
});
|
|
247
338
|
|
|
@@ -258,20 +349,27 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
258
349
|
|
|
259
350
|
await writeReasoning(sink, "thinking");
|
|
260
351
|
|
|
261
|
-
expect(JSON.parse(emitted[0]!).choices[0].delta.reasoning_content).toBe(
|
|
352
|
+
expect(JSON.parse(emitted[0]!).choices[0].delta.reasoning_content).toBe(
|
|
353
|
+
"thinking",
|
|
354
|
+
);
|
|
262
355
|
});
|
|
263
356
|
|
|
264
357
|
it("retries executor failures", async () => {
|
|
265
358
|
let calls = 0;
|
|
266
|
-
const executor = retry(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
359
|
+
const executor = retry(
|
|
360
|
+
executorFromExecute(async () => {
|
|
361
|
+
calls += 1;
|
|
362
|
+
if (calls === 1) {
|
|
363
|
+
throw new Error("transient");
|
|
364
|
+
}
|
|
365
|
+
return appendAssistantMessage({ code: [], buffers: [] }, "ok");
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
273
368
|
|
|
274
|
-
const result = await executor.execute(
|
|
369
|
+
const result = await executor.execute(
|
|
370
|
+
{ code: [], buffers: [] },
|
|
371
|
+
buildExecutorContext(),
|
|
372
|
+
);
|
|
275
373
|
|
|
276
374
|
expect(calls).toBe(2);
|
|
277
375
|
expect(contentText(result)).toBe("ok");
|
|
@@ -282,23 +380,75 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
282
380
|
executorFromExecute(async () => {
|
|
283
381
|
throw new Error("primary failed");
|
|
284
382
|
}),
|
|
285
|
-
executorFromExecute(async () =>
|
|
383
|
+
executorFromExecute(async () =>
|
|
384
|
+
appendAssistantMessage({ code: [], buffers: [] }, "fallback"),
|
|
385
|
+
),
|
|
286
386
|
]);
|
|
287
387
|
|
|
288
|
-
const result = await executor.execute(
|
|
388
|
+
const result = await executor.execute(
|
|
389
|
+
{ code: [], buffers: [] },
|
|
390
|
+
buildExecutorContext(),
|
|
391
|
+
);
|
|
289
392
|
|
|
290
393
|
expect(contentText(result)).toBe("fallback");
|
|
291
394
|
});
|
|
292
395
|
|
|
293
|
-
it("
|
|
396
|
+
it("does not retry a stream after exposing a partial chunk", async () => {
|
|
397
|
+
let retryCalls = 0;
|
|
398
|
+
const executor = retry(
|
|
399
|
+
streamingExecutor(async function* () {
|
|
400
|
+
retryCalls += 1;
|
|
401
|
+
yield appendAssistantMessage({ code: [], buffers: [] }, "partial");
|
|
402
|
+
throw new Error("stream interrupted");
|
|
403
|
+
}),
|
|
404
|
+
{ attempts: 2 },
|
|
405
|
+
);
|
|
406
|
+
const context = buildExecutorContext();
|
|
407
|
+
const stream = executor
|
|
408
|
+
.stream({ code: [], buffers: [] }, context)
|
|
409
|
+
[Symbol.asyncIterator]();
|
|
410
|
+
|
|
411
|
+
await expect(stream.next()).resolves.toMatchObject({ done: false });
|
|
412
|
+
await expect(stream.next()).rejects.toThrow("stream interrupted");
|
|
413
|
+
expect(retryCalls).toBe(1);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("does not fall back after exposing a partial stream chunk", async () => {
|
|
417
|
+
let fallbackCalls = 0;
|
|
418
|
+
const executor = fallback([
|
|
419
|
+
streamingExecutor(async function* () {
|
|
420
|
+
yield appendAssistantMessage({ code: [], buffers: [] }, "partial");
|
|
421
|
+
throw new Error("stream interrupted");
|
|
422
|
+
}),
|
|
423
|
+
streamingExecutor(async function* () {
|
|
424
|
+
fallbackCalls += 1;
|
|
425
|
+
yield appendAssistantMessage({ code: [], buffers: [] }, "fallback");
|
|
426
|
+
}),
|
|
427
|
+
]);
|
|
428
|
+
const stream = executor
|
|
429
|
+
.stream({ code: [], buffers: [] }, buildExecutorContext())
|
|
430
|
+
[Symbol.asyncIterator]();
|
|
431
|
+
|
|
432
|
+
await expect(stream.next()).resolves.toMatchObject({ done: false });
|
|
433
|
+
await expect(stream.next()).rejects.toThrow("stream interrupted");
|
|
434
|
+
expect(fallbackCalls).toBe(0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("runs execute-only goal loops until the review is satisfied", async () => {
|
|
294
438
|
let attempts = 0;
|
|
295
|
-
const executor =
|
|
439
|
+
const executor = runGoal({
|
|
296
440
|
draft: executorFromExecute(async () => {
|
|
297
441
|
attempts += 1;
|
|
298
|
-
return appendAssistantMessage(
|
|
442
|
+
return appendAssistantMessage(
|
|
443
|
+
{ code: [], buffers: [] },
|
|
444
|
+
`answer ${attempts}`,
|
|
445
|
+
);
|
|
299
446
|
}),
|
|
300
447
|
review: executorFromExecute(async () =>
|
|
301
|
-
appendAssistantMessage(
|
|
448
|
+
appendAssistantMessage(
|
|
449
|
+
{ code: [], buffers: [] },
|
|
450
|
+
attempts > 1 ? "pass" : "retry",
|
|
451
|
+
),
|
|
302
452
|
),
|
|
303
453
|
satisfied(review) {
|
|
304
454
|
return contentText(review) === "pass";
|
|
@@ -308,7 +458,10 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
308
458
|
},
|
|
309
459
|
});
|
|
310
460
|
|
|
311
|
-
const result = await executor.execute(
|
|
461
|
+
const result = await executor.execute(
|
|
462
|
+
{ code: [], buffers: [] },
|
|
463
|
+
buildExecutorContext(),
|
|
464
|
+
);
|
|
312
465
|
|
|
313
466
|
expect(attempts).toBe(2);
|
|
314
467
|
expect(contentText(result)).toBe("answer 2");
|
|
@@ -343,3 +496,14 @@ function executorFromExecute(execute: Executor["execute"]): Executor {
|
|
|
343
496
|
},
|
|
344
497
|
};
|
|
345
498
|
}
|
|
499
|
+
|
|
500
|
+
function streamingExecutor(stream: Executor["stream"]): Executor {
|
|
501
|
+
return {
|
|
502
|
+
async execute(request, ctx) {
|
|
503
|
+
let result = request;
|
|
504
|
+
for await (const chunk of stream(request, ctx)) result = chunk;
|
|
505
|
+
return result;
|
|
506
|
+
},
|
|
507
|
+
stream,
|
|
508
|
+
};
|
|
509
|
+
}
|