@neutrome/lilsdk-ts 0.1.3 → 0.2.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.
- package/README.md +19 -11
- package/package.json +10 -3
- package/src/index.ts +14 -9
- package/src/loops/index.ts +190 -0
- package/src/managed/index.ts +15 -0
- package/src/managed/twoPassExecutor.ts +214 -0
- package/src/managed/wrap.ts +17 -0
- package/src/observe.ts +94 -2
- package/src/output.ts +18 -3
- package/src/primitives/index.ts +120 -0
- package/src/stream/index.ts +4 -0
- package/src/synthetic/index.ts +151 -0
- package/src/tools.ts +357 -0
- package/src/types.ts +82 -20
- package/test/lilsdk-ts.test.ts +244 -84
- package/test/tools.test.ts +259 -0
- package/src/execution.ts +0 -64
- package/src/request.ts +0 -21
package/src/types.ts
CHANGED
|
@@ -1,29 +1,91 @@
|
|
|
1
1
|
import type { Program } from "@neutrome/lil-engine";
|
|
2
|
-
import type { ExecutionRuntime, InvokeOptions } from "@neutrome/lil-engine";
|
|
3
2
|
|
|
4
|
-
export type
|
|
3
|
+
export type TransformCapability =
|
|
4
|
+
| "read_messages"
|
|
5
|
+
| "write_messages"
|
|
6
|
+
| "write_config"
|
|
7
|
+
| "write_tools"
|
|
8
|
+
| "drop_content"
|
|
9
|
+
| "provider_extension";
|
|
5
10
|
|
|
6
|
-
export type
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
export type SdkEvent = {
|
|
26
|
+
kind: string;
|
|
27
|
+
requestId: string;
|
|
28
|
+
executionId: string;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
parentExecutionId?: string;
|
|
31
|
+
target?: { kind: "provider" | "executor"; id: string };
|
|
32
|
+
errorKind?: string;
|
|
33
|
+
data?: Record<string, unknown>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type SdkEventInput = Omit<SdkEvent, "timestamp"> & {
|
|
37
|
+
timestamp?: string;
|
|
9
38
|
};
|
|
10
39
|
|
|
11
|
-
export type
|
|
40
|
+
export type InvokeOptions = {
|
|
41
|
+
target?: ExecutionTarget;
|
|
42
|
+
requestId?: string;
|
|
43
|
+
executionId?: string;
|
|
44
|
+
parentExecutionId?: string;
|
|
45
|
+
};
|
|
12
46
|
|
|
13
|
-
export type
|
|
14
|
-
|
|
47
|
+
export type TransformContext = {
|
|
48
|
+
observe(event: SdkEvent): void;
|
|
49
|
+
signal: AbortSignal;
|
|
15
50
|
};
|
|
16
51
|
|
|
17
|
-
export type
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
options?: ExecuteModelOptions,
|
|
22
|
-
): Promise<Program>;
|
|
23
|
-
stream(
|
|
24
|
-
request: Program,
|
|
25
|
-
runtime: ModelRuntime,
|
|
26
|
-
sink: OutputSink,
|
|
27
|
-
options?: StreamModelOptions,
|
|
28
|
-
): Promise<void>;
|
|
52
|
+
export type ProgramTransform = {
|
|
53
|
+
name: string;
|
|
54
|
+
capabilities: TransformCapability[];
|
|
55
|
+
apply(program: Program, ctx: TransformContext): Program;
|
|
29
56
|
};
|
|
57
|
+
|
|
58
|
+
export type ExecutorContext = {
|
|
59
|
+
requestId: string;
|
|
60
|
+
executionId: string;
|
|
61
|
+
parentExecutionId?: string;
|
|
62
|
+
invoke(request: Program, options?: InvokeOptions): Promise<Program>;
|
|
63
|
+
invokeStream(request: Program, options?: InvokeOptions): AsyncIterable<Program>;
|
|
64
|
+
observe(event: SdkEvent): void;
|
|
65
|
+
signal: AbortSignal;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type Executor = {
|
|
69
|
+
execute(request: Program, ctx: ExecutorContext): Promise<Program>;
|
|
70
|
+
stream(request: Program, ctx: ExecutorContext): AsyncIterable<Program>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type Tool = {
|
|
74
|
+
name: string;
|
|
75
|
+
description: string;
|
|
76
|
+
schema: Record<string, unknown>;
|
|
77
|
+
systemPromptFragment?: string;
|
|
78
|
+
execute(args: Record<string, unknown>, ctx: ExecutorContext): Promise<string>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type OutputSink = {
|
|
82
|
+
write(chunk: Program): void | Promise<void>;
|
|
83
|
+
close(): void | Promise<void>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function createSdkEvent(input: SdkEventInput): SdkEvent {
|
|
87
|
+
return {
|
|
88
|
+
...input,
|
|
89
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
}
|
package/test/lilsdk-ts.test.ts
CHANGED
|
@@ -1,32 +1,58 @@
|
|
|
1
1
|
import {
|
|
2
|
-
appendAssistantMessage,
|
|
3
2
|
contentText,
|
|
4
3
|
emitChatCompletionsRequest,
|
|
5
4
|
emitChatCompletionsStreamChunk,
|
|
6
5
|
getModel,
|
|
7
|
-
isStreaming,
|
|
8
6
|
parseChatCompletionsRequest,
|
|
9
7
|
parseChatCompletionsStreamChunk,
|
|
10
|
-
|
|
8
|
+
Opcode,
|
|
11
9
|
type Program,
|
|
12
10
|
} from "@neutrome/lil-engine";
|
|
13
11
|
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { all, any, find, has, indexOf, map, reduce } from "../src/primitives/index.ts";
|
|
13
|
+
import { appendAssistantMessage, appendToolInteraction } from "../src/synthetic/index.ts";
|
|
14
|
+
import { observeExecutionStream, writeReasoning } from "../src/stream/index.ts";
|
|
15
|
+
import { fallback, goal, retry } from "../src/loops/index.ts";
|
|
14
16
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
appendInternalDraft,
|
|
18
|
+
createTwoPassExecutor,
|
|
19
|
+
invokeModel,
|
|
20
|
+
INTERNAL_DRAFT_TOOL_NAME,
|
|
21
|
+
streamModel,
|
|
22
|
+
} from "../src/managed/twoPassExecutor.ts";
|
|
23
|
+
import type {
|
|
24
|
+
Executor,
|
|
25
|
+
ExecutorContext,
|
|
26
|
+
OutputSink,
|
|
27
|
+
} from "../src/types.ts";
|
|
24
28
|
|
|
25
29
|
const encoder = new TextEncoder();
|
|
26
30
|
const decoder = new TextDecoder();
|
|
27
31
|
|
|
28
32
|
describe("@neutrome/lilsdk-ts", () => {
|
|
29
|
-
it("
|
|
33
|
+
it("provides clone-safe structural primitives", () => {
|
|
34
|
+
const needle = { opcode: Opcode.MSG_START, value: { kind: "none" as const } };
|
|
35
|
+
const program: Program = {
|
|
36
|
+
code: [
|
|
37
|
+
needle,
|
|
38
|
+
{ opcode: Opcode.TXT_CHUNK, value: { kind: "string", value: "hello" } },
|
|
39
|
+
],
|
|
40
|
+
buffers: [new Uint8Array([1, 2, 3])],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
expect(indexOf(program, needle)).toBe(0);
|
|
44
|
+
expect(has(program, needle)).toBe(true);
|
|
45
|
+
expect(find(program, needle)).toEqual(needle);
|
|
46
|
+
expect(reduce(program, (count) => count + 1, 0)).toBe(2);
|
|
47
|
+
expect(all(program, (instruction) => instruction.opcode !== Opcode.MSG_END)).toBe(true);
|
|
48
|
+
expect(any(program, (instruction) => instruction.opcode === Opcode.TXT_CHUNK)).toBe(true);
|
|
49
|
+
|
|
50
|
+
const mapped = map(program, (instruction) => instruction);
|
|
51
|
+
mapped.buffers[0]![0] = 9;
|
|
52
|
+
expect(program.buffers[0]![0]).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("observes tool-call streams", async () => {
|
|
30
56
|
const observed = await observeExecutionStream((async function* () {
|
|
31
57
|
yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
|
|
32
58
|
id: "tool-stream",
|
|
@@ -63,88 +89,163 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
63
89
|
throw new Error("expected tool mode");
|
|
64
90
|
}
|
|
65
91
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
const parsed = observed.chunks.map((chunk) =>
|
|
93
|
+
JSON.parse(decoder.decode(emitChatCompletionsStreamChunk(chunk)))
|
|
94
|
+
);
|
|
95
|
+
expect(parsed.some((chunk) => chunk.choices?.[0]?.delta?.tool_calls)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("appends internal drafts with fixed SDK metadata and empty args", () => {
|
|
99
|
+
const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
|
|
100
|
+
model: "virtual-model",
|
|
101
|
+
messages: [{ role: "user", content: "hello" }],
|
|
102
|
+
})));
|
|
75
103
|
|
|
76
|
-
|
|
104
|
+
const updated = appendInternalDraft(request, "draft answer");
|
|
105
|
+
const emitted = JSON.parse(decoder.decode(emitChatCompletionsRequest(updated)));
|
|
77
106
|
|
|
78
|
-
expect(emitted.
|
|
79
|
-
|
|
80
|
-
expect(
|
|
107
|
+
expect(emitted.messages[1].tool_calls[0].function.name).toBe(INTERNAL_DRAFT_TOOL_NAME);
|
|
108
|
+
expect(emitted.messages[1].tool_calls[0].function.arguments).toBe("{}");
|
|
109
|
+
expect(emitted.messages[2].content).toBe("draft answer");
|
|
81
110
|
});
|
|
82
111
|
|
|
83
|
-
it("appends synthetic tool
|
|
112
|
+
it("appends generic synthetic tool interactions", () => {
|
|
84
113
|
const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
|
|
85
114
|
model: "virtual-model",
|
|
86
115
|
messages: [{ role: "user", content: "hello" }],
|
|
87
116
|
})));
|
|
88
117
|
|
|
89
|
-
const updated =
|
|
90
|
-
callId: "
|
|
91
|
-
name: "
|
|
92
|
-
args:
|
|
93
|
-
result: "
|
|
118
|
+
const updated = appendToolInteraction(request, {
|
|
119
|
+
callId: "call_lookup",
|
|
120
|
+
name: "lookup",
|
|
121
|
+
args: { id: "42" },
|
|
122
|
+
result: { status: "active" },
|
|
94
123
|
});
|
|
95
|
-
|
|
96
124
|
const emitted = JSON.parse(decoder.decode(emitChatCompletionsRequest(updated)));
|
|
97
|
-
|
|
98
|
-
expect(emitted.messages[1].tool_calls[0].function
|
|
99
|
-
|
|
100
|
-
|
|
125
|
+
|
|
126
|
+
expect(emitted.messages[1].tool_calls[0].function).toEqual({
|
|
127
|
+
name: "lookup",
|
|
128
|
+
arguments: "{\"id\":\"42\"}",
|
|
129
|
+
});
|
|
130
|
+
expect(emitted.messages[2]).toEqual({
|
|
131
|
+
role: "tool",
|
|
132
|
+
tool_call_id: "call_lookup",
|
|
133
|
+
content: "{\"status\":\"active\"}",
|
|
134
|
+
});
|
|
101
135
|
});
|
|
102
136
|
|
|
103
|
-
it("
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
it("builds two-pass executors from model and prompt config", async () => {
|
|
138
|
+
const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
|
|
139
|
+
model: "virtual-model",
|
|
140
|
+
messages: [{ role: "user", content: "hello" }],
|
|
141
|
+
})));
|
|
142
|
+
const seen: Program[] = [];
|
|
143
|
+
const ctx = buildExecutorContext({
|
|
144
|
+
async invoke(program) {
|
|
145
|
+
seen.push(program);
|
|
146
|
+
if (seen.length === 1) {
|
|
109
147
|
return appendAssistantMessage({ code: [], buffers: [] }, "draft answer");
|
|
110
148
|
}
|
|
111
149
|
return appendAssistantMessage({ code: [], buffers: [] }, "final answer");
|
|
112
150
|
},
|
|
113
|
-
async *
|
|
151
|
+
async *invokeStream() {
|
|
114
152
|
throw new Error("streaming path is not used in this test");
|
|
115
153
|
},
|
|
116
|
-
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const executor = createTwoPassExecutor({
|
|
157
|
+
draftModel: "smart-model",
|
|
158
|
+
finalModel: "base-model",
|
|
159
|
+
finalSystemPrompt: "system prompt",
|
|
160
|
+
});
|
|
117
161
|
|
|
162
|
+
const result = await executor.execute(request, ctx);
|
|
163
|
+
|
|
164
|
+
expect(seen.map((program) => getModel(program))).toEqual(["smart-model", "base-model"]);
|
|
165
|
+
expect(contentText(result)).toBe("final answer");
|
|
166
|
+
|
|
167
|
+
const finalRequest = JSON.parse(decoder.decode(emitChatCompletionsRequest(seen[1]!)));
|
|
168
|
+
expect(finalRequest.messages[0]).toEqual({ role: "system", content: "system prompt" });
|
|
169
|
+
expect(finalRequest.messages[2].tool_calls[0].function).toEqual({
|
|
170
|
+
name: INTERNAL_DRAFT_TOOL_NAME,
|
|
171
|
+
arguments: "{}",
|
|
172
|
+
});
|
|
173
|
+
expect(finalRequest.messages[3]).toEqual({
|
|
174
|
+
role: "tool",
|
|
175
|
+
tool_call_id: "knowledge_0",
|
|
176
|
+
content: "draft answer",
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("replaces draft system prompt and moves existing system prompt to first user message", async () => {
|
|
118
181
|
const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
|
|
119
182
|
model: "virtual-model",
|
|
120
|
-
messages: [
|
|
183
|
+
messages: [
|
|
184
|
+
{ role: "system", content: "original system" },
|
|
185
|
+
{ role: "user", content: "hello" },
|
|
186
|
+
],
|
|
121
187
|
})));
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
188
|
+
const seen: Program[] = [];
|
|
189
|
+
const ctx = buildExecutorContext({
|
|
190
|
+
async invoke(program) {
|
|
191
|
+
seen.push(program);
|
|
192
|
+
return appendAssistantMessage({ code: [], buffers: [] }, seen.length === 1 ? "draft" : "final");
|
|
193
|
+
},
|
|
194
|
+
async *invokeStream() {
|
|
195
|
+
throw new Error("streaming path is not used in this test");
|
|
196
|
+
},
|
|
130
197
|
});
|
|
131
|
-
secondRequest = prependSystemPrompt(secondRequest, "system prompt");
|
|
132
198
|
|
|
133
|
-
const
|
|
199
|
+
const executor = createTwoPassExecutor({
|
|
200
|
+
draftModel: "smart-model",
|
|
201
|
+
finalModel: "base-model",
|
|
202
|
+
draftSystemPrompt: "draft system",
|
|
203
|
+
});
|
|
134
204
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
205
|
+
await executor.execute(request, ctx);
|
|
206
|
+
|
|
207
|
+
const draftRequest = JSON.parse(decoder.decode(emitChatCompletionsRequest(seen[0]!)));
|
|
208
|
+
expect(draftRequest.messages).toEqual([
|
|
209
|
+
{ role: "system", content: "draft system" },
|
|
210
|
+
{ role: "user", content: "original system" },
|
|
211
|
+
{ role: "user", content: "hello" },
|
|
138
212
|
]);
|
|
139
|
-
expect(contentText(second)).toBe("final answer");
|
|
140
|
-
expect(getModel(request)).toBe("virtual-model");
|
|
141
213
|
});
|
|
142
214
|
|
|
143
|
-
it("streams model
|
|
215
|
+
it("invokes and streams another model through executor context helpers", async () => {
|
|
216
|
+
const calls: Array<{ model: string; streaming: boolean }> = [];
|
|
217
|
+
const ctx = buildExecutorContext({
|
|
218
|
+
async invoke(program) {
|
|
219
|
+
calls.push({ model: getModel(program) ?? "", streaming: false });
|
|
220
|
+
return appendAssistantMessage({ code: [], buffers: [] }, "model answer");
|
|
221
|
+
},
|
|
222
|
+
async *invokeStream(program) {
|
|
223
|
+
calls.push({ model: getModel(program) ?? "", streaming: true });
|
|
224
|
+
yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
|
|
225
|
+
id: "stream-response",
|
|
226
|
+
model: "base-model",
|
|
227
|
+
choices: [{ index: 0, delta: { content: "hello" } }],
|
|
228
|
+
})));
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
144
232
|
const request = parseChatCompletionsRequest(encoder.encode(JSON.stringify({
|
|
145
233
|
model: "virtual-model",
|
|
146
234
|
messages: [{ role: "user", content: "hello" }],
|
|
147
235
|
})));
|
|
236
|
+
const first = await invokeModel(ctx, request, "smart-model");
|
|
237
|
+
const chunks: Program[] = [];
|
|
238
|
+
for await (const chunk of streamModel(ctx, request, "base-model")) {
|
|
239
|
+
chunks.push(chunk);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
expect(calls).toEqual([{ model: "smart-model", streaming: false }, { model: "base-model", streaming: true }]);
|
|
243
|
+
expect(contentText(first)).toBe("model answer");
|
|
244
|
+
expect(decoder.decode(emitChatCompletionsStreamChunk(chunks[0]!))).toContain("hello");
|
|
245
|
+
expect(getModel(request)).toBe("virtual-model");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("writes reasoning helpers to a sink", async () => {
|
|
148
249
|
const emitted: string[] = [];
|
|
149
250
|
const sink: OutputSink = {
|
|
150
251
|
write(chunk) {
|
|
@@ -155,31 +256,90 @@ describe("@neutrome/lilsdk-ts", () => {
|
|
|
155
256
|
},
|
|
156
257
|
};
|
|
157
258
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
259
|
+
await writeReasoning(sink, "thinking");
|
|
260
|
+
|
|
261
|
+
expect(JSON.parse(emitted[0]!).choices[0].delta.reasoning_content).toBe("thinking");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("retries executor failures", async () => {
|
|
265
|
+
let calls = 0;
|
|
266
|
+
const executor = retry(executorFromExecute(async () => {
|
|
267
|
+
calls += 1;
|
|
268
|
+
if (calls === 1) {
|
|
269
|
+
throw new Error("transient");
|
|
270
|
+
}
|
|
271
|
+
return appendAssistantMessage({ code: [], buffers: [] }, "ok");
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
const result = await executor.execute({ code: [], buffers: [] }, buildExecutorContext());
|
|
275
|
+
|
|
276
|
+
expect(calls).toBe(2);
|
|
277
|
+
expect(contentText(result)).toBe("ok");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("falls back to the next executor after failure", async () => {
|
|
281
|
+
const executor = fallback([
|
|
282
|
+
executorFromExecute(async () => {
|
|
283
|
+
throw new Error("primary failed");
|
|
284
|
+
}),
|
|
285
|
+
executorFromExecute(async () => appendAssistantMessage({ code: [], buffers: [] }, "fallback")),
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
const result = await executor.execute({ code: [], buffers: [] }, buildExecutorContext());
|
|
289
|
+
|
|
290
|
+
expect(contentText(result)).toBe("fallback");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("runs goal loops until the review is satisfied", async () => {
|
|
294
|
+
let attempts = 0;
|
|
295
|
+
const executor = goal({
|
|
296
|
+
draft: executorFromExecute(async () => {
|
|
297
|
+
attempts += 1;
|
|
298
|
+
return appendAssistantMessage({ code: [], buffers: [] }, `answer ${attempts}`);
|
|
299
|
+
}),
|
|
300
|
+
review: executorFromExecute(async () =>
|
|
301
|
+
appendAssistantMessage({ code: [], buffers: [] }, attempts > 1 ? "pass" : "retry")
|
|
302
|
+
),
|
|
303
|
+
satisfied(review) {
|
|
304
|
+
return contentText(review) === "pass";
|
|
161
305
|
},
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(isStreaming(program)).toBe(true);
|
|
165
|
-
yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
|
|
166
|
-
id: "stream-response",
|
|
167
|
-
model: "base-model",
|
|
168
|
-
choices: [{ index: 0, delta: { content: "hello" } }],
|
|
169
|
-
})));
|
|
170
|
-
yield parseChatCompletionsStreamChunk(encoder.encode(JSON.stringify({
|
|
171
|
-
id: "stream-response",
|
|
172
|
-
model: "base-model",
|
|
173
|
-
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
174
|
-
})));
|
|
306
|
+
refine(request) {
|
|
307
|
+
return request;
|
|
175
308
|
},
|
|
176
|
-
};
|
|
309
|
+
});
|
|
177
310
|
|
|
178
|
-
await
|
|
179
|
-
await streamWithModel(request, runtime, sink, "base-model");
|
|
311
|
+
const result = await executor.execute({ code: [], buffers: [] }, buildExecutorContext());
|
|
180
312
|
|
|
181
|
-
expect(
|
|
182
|
-
expect(
|
|
183
|
-
expect(JSON.parse(emitted[1]!).choices[0].delta.content).toBe("hello");
|
|
313
|
+
expect(attempts).toBe(2);
|
|
314
|
+
expect(contentText(result)).toBe("answer 2");
|
|
184
315
|
});
|
|
185
316
|
});
|
|
317
|
+
|
|
318
|
+
function buildExecutorContext(
|
|
319
|
+
impl: Pick<ExecutorContext, "invoke" | "invokeStream"> = {
|
|
320
|
+
async invoke(program) {
|
|
321
|
+
return program;
|
|
322
|
+
},
|
|
323
|
+
async *invokeStream(program) {
|
|
324
|
+
yield program;
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
): ExecutorContext {
|
|
328
|
+
return {
|
|
329
|
+
requestId: "req_test",
|
|
330
|
+
executionId: "exec_test",
|
|
331
|
+
invoke: impl.invoke,
|
|
332
|
+
invokeStream: impl.invokeStream,
|
|
333
|
+
observe: () => {},
|
|
334
|
+
signal: new AbortController().signal,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function executorFromExecute(execute: Executor["execute"]): Executor {
|
|
339
|
+
return {
|
|
340
|
+
execute,
|
|
341
|
+
async *stream(request, ctx) {
|
|
342
|
+
yield await execute(request, ctx);
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|