@neutrome/lilsdk 0.6.3 → 0.6.5
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 +1 -1
- package/src/managed/collapsed-reasoner.ts +126 -35
- package/test/collapsed-reasoner.test.ts +164 -22
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
findThinkingBlocks,
|
|
8
8
|
Opcode,
|
|
9
9
|
prependSystemPrompt,
|
|
10
|
+
ProgramView,
|
|
10
11
|
removeInstructions,
|
|
11
12
|
type Program,
|
|
12
13
|
} from "@neutrome/lil-engine";
|
|
@@ -19,9 +20,13 @@ const DEFAULT_PROMPT =
|
|
|
19
20
|
"Summarize the reasoning below as a concise user-facing progress update. Return an empty response when there is no useful update.";
|
|
20
21
|
const DEFAULT_FORCED_PROMPT =
|
|
21
22
|
"Give a concise user-facing progress update based only on the reasoning below. Return an empty response when no update is useful.";
|
|
23
|
+
const INITIAL_PROMPT =
|
|
24
|
+
"Based only on the user's request below, give one concise user-facing update about what you are starting. Do not claim progress or results.";
|
|
22
25
|
|
|
23
26
|
/** Options accepted by {@link createCollapsedReasoner}. */
|
|
24
27
|
export type CollapsedReasonerOptions = {
|
|
28
|
+
/** Prompt used to initialize the reasoner's state. */
|
|
29
|
+
initialPrompt?: string;
|
|
25
30
|
/** Prompt used for periodic progress updates. */
|
|
26
31
|
prompt?: string;
|
|
27
32
|
/** Prompt used when an update must be attempted. */
|
|
@@ -52,21 +57,30 @@ export function createCollapsedReasoner(
|
|
|
52
57
|
const response = await ctx.invoke(reasoner, request);
|
|
53
58
|
const reasoning = extractThinkingText(response);
|
|
54
59
|
const concealed = withoutThinking(response);
|
|
55
|
-
if (!reasoning) return concealed;
|
|
60
|
+
if (!reasoning || extractContentText(concealed)) return concealed;
|
|
56
61
|
|
|
57
|
-
const summary = await summarize(
|
|
62
|
+
const summary = await summarize(
|
|
63
|
+
ctx,
|
|
64
|
+
summarizer,
|
|
65
|
+
settings.forcedPrompt,
|
|
66
|
+
reasoning,
|
|
67
|
+
);
|
|
58
68
|
return summary ? appendAssistantThinking(concealed, summary) : concealed;
|
|
59
69
|
},
|
|
60
70
|
|
|
61
71
|
async *stream(request, ctx) {
|
|
62
|
-
const source = ctx
|
|
72
|
+
const source = ctx
|
|
73
|
+
.invokeStream(reasoner, request)
|
|
74
|
+
[Symbol.asyncIterator]();
|
|
63
75
|
let raw = "";
|
|
64
76
|
let startedAt: number | undefined;
|
|
65
77
|
let normalAt = 0;
|
|
66
78
|
let forcedAt = 0;
|
|
67
79
|
let summaries: Promise<void> = Promise.resolve();
|
|
68
80
|
const ready: string[] = [];
|
|
69
|
-
let
|
|
81
|
+
let phase = 0;
|
|
82
|
+
let acceptsSummaries = true;
|
|
83
|
+
let summaryError: { phase: number; error: unknown } | undefined;
|
|
70
84
|
let wake: (() => void) | undefined;
|
|
71
85
|
let wakePromise = nextWake();
|
|
72
86
|
const terminal: Program[] = [];
|
|
@@ -75,16 +89,30 @@ export function createCollapsedReasoner(
|
|
|
75
89
|
wake?.();
|
|
76
90
|
wakePromise = nextWake();
|
|
77
91
|
};
|
|
92
|
+
const initial = summarizeInitial(ctx, summarizer, request, settings.initialPrompt).then(
|
|
93
|
+
(summary) => {
|
|
94
|
+
if (summary && acceptsSummaries && phase === 0) ready.push(summary);
|
|
95
|
+
notify();
|
|
96
|
+
},
|
|
97
|
+
(error: unknown) => {
|
|
98
|
+
if (acceptsSummaries && phase === 0)
|
|
99
|
+
summaryError = { phase: 0, error };
|
|
100
|
+
notify();
|
|
101
|
+
},
|
|
102
|
+
);
|
|
78
103
|
const enqueue = (prompt: string, force: boolean) => {
|
|
79
|
-
if (!raw) return;
|
|
104
|
+
if (!acceptsSummaries || !raw) return;
|
|
80
105
|
const interval = raw;
|
|
106
|
+
const summaryPhase = phase;
|
|
81
107
|
raw = "";
|
|
82
108
|
summaries = summaries.then(async () => {
|
|
83
109
|
const summary = await summarize(ctx, summarizer, prompt, interval);
|
|
84
|
-
if (summary
|
|
110
|
+
if (summary && acceptsSummaries && phase === summaryPhase)
|
|
111
|
+
ready.push(summary);
|
|
85
112
|
});
|
|
86
113
|
void summaries.then(notify, (error: unknown) => {
|
|
87
|
-
|
|
114
|
+
if (acceptsSummaries && phase === summaryPhase)
|
|
115
|
+
summaryError = { phase: summaryPhase, error };
|
|
88
116
|
notify();
|
|
89
117
|
});
|
|
90
118
|
if (force) normalAt = Date.now();
|
|
@@ -93,11 +121,12 @@ export function createCollapsedReasoner(
|
|
|
93
121
|
let next = source.next();
|
|
94
122
|
try {
|
|
95
123
|
for (;;) {
|
|
96
|
-
if (summaryError) throw summaryError;
|
|
97
|
-
while (ready.length > 0)
|
|
124
|
+
if (summaryError?.phase === phase) throw summaryError.error;
|
|
125
|
+
while (acceptsSummaries && ready.length > 0)
|
|
126
|
+
yield thinkingChunk(ready.shift()!);
|
|
98
127
|
|
|
99
128
|
const now = Date.now();
|
|
100
|
-
if (startedAt !== undefined) {
|
|
129
|
+
if (acceptsSummaries && startedAt !== undefined) {
|
|
101
130
|
if (now >= forcedAt) {
|
|
102
131
|
enqueue(settings.forcedPrompt, true);
|
|
103
132
|
forcedAt = now + settings.forcedInterval;
|
|
@@ -111,34 +140,55 @@ export function createCollapsedReasoner(
|
|
|
111
140
|
}
|
|
112
141
|
}
|
|
113
142
|
|
|
114
|
-
const due =
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
const due =
|
|
144
|
+
!acceptsSummaries || startedAt === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: Math.max(0, Math.min(normalAt, forcedAt) - Date.now());
|
|
117
147
|
const event = await Promise.race([
|
|
118
148
|
next.then((result) => ({ type: "source" as const, result })),
|
|
119
149
|
wakePromise.then(() => ({ type: "summary" as const })),
|
|
120
|
-
...(due === undefined
|
|
150
|
+
...(due === undefined
|
|
151
|
+
? []
|
|
152
|
+
: [delay(due).then(() => ({ type: "timer" as const }))]),
|
|
121
153
|
]);
|
|
122
154
|
if (event.type !== "source") continue;
|
|
123
155
|
next = source.next();
|
|
124
156
|
if (event.result.done) break;
|
|
125
157
|
|
|
126
|
-
const visible = splitStreamChunk(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
const visible = splitStreamChunk(
|
|
159
|
+
event.result.value,
|
|
160
|
+
terminal,
|
|
161
|
+
(text) => {
|
|
162
|
+
if (!text) return;
|
|
163
|
+
if (!acceptsSummaries) acceptsSummaries = true;
|
|
164
|
+
raw += text;
|
|
165
|
+
if (startedAt === undefined) {
|
|
166
|
+
const timestamp = Date.now();
|
|
167
|
+
startedAt = timestamp;
|
|
168
|
+
normalAt = timestamp + settings.interval;
|
|
169
|
+
forcedAt = timestamp + settings.forcedInterval;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
(text) => {
|
|
173
|
+
if (!text || !acceptsSummaries) return;
|
|
174
|
+
acceptsSummaries = false;
|
|
175
|
+
phase += 1;
|
|
176
|
+
raw = "";
|
|
177
|
+
startedAt = undefined;
|
|
178
|
+
ready.length = 0;
|
|
179
|
+
summaries = Promise.resolve();
|
|
180
|
+
},
|
|
181
|
+
);
|
|
135
182
|
if (visible.code.length > 0) yield visible;
|
|
136
183
|
}
|
|
137
184
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
185
|
+
if (acceptsSummaries) {
|
|
186
|
+
enqueue(settings.forcedPrompt, true);
|
|
187
|
+
await Promise.all([summaries, ...(phase === 0 ? [initial] : [])]);
|
|
188
|
+
}
|
|
189
|
+
if (summaryError?.phase === phase) throw summaryError.error;
|
|
190
|
+
if (acceptsSummaries)
|
|
191
|
+
while (ready.length > 0) yield thinkingChunk(ready.shift()!);
|
|
142
192
|
yield* terminal;
|
|
143
193
|
} finally {
|
|
144
194
|
await source.return?.();
|
|
@@ -154,17 +204,23 @@ export function createCollapsedReasoner(
|
|
|
154
204
|
}
|
|
155
205
|
|
|
156
206
|
function parseOptions(options: CollapsedReasonerOptions) {
|
|
157
|
-
const interval = positiveDuration(
|
|
207
|
+
const interval = positiveDuration(
|
|
208
|
+
options.interval ?? DEFAULT_INTERVAL,
|
|
209
|
+
"interval",
|
|
210
|
+
);
|
|
158
211
|
const forcedInterval = positiveDuration(
|
|
159
212
|
options.forcedInterval ?? DEFAULT_FORCED_INTERVAL,
|
|
160
213
|
"forcedInterval",
|
|
161
214
|
);
|
|
162
215
|
if (forcedInterval < interval) {
|
|
163
|
-
throw new RangeError(
|
|
216
|
+
throw new RangeError(
|
|
217
|
+
"forcedInterval must be greater than or equal to interval",
|
|
218
|
+
);
|
|
164
219
|
}
|
|
165
220
|
return {
|
|
166
221
|
interval,
|
|
167
222
|
forcedInterval,
|
|
223
|
+
initialPrompt: options.initialPrompt ?? INITIAL_PROMPT,
|
|
168
224
|
prompt: options.prompt ?? DEFAULT_PROMPT,
|
|
169
225
|
forcedPrompt: options.forcedPrompt ?? DEFAULT_FORCED_PROMPT,
|
|
170
226
|
};
|
|
@@ -183,13 +239,32 @@ async function summarize(
|
|
|
183
239
|
prompt: string,
|
|
184
240
|
reasoning: string,
|
|
185
241
|
): Promise<string> {
|
|
186
|
-
const request = prependSystemPrompt(
|
|
242
|
+
const request = prependSystemPrompt(
|
|
243
|
+
appendUserMessage(createProgram(), reasoning),
|
|
244
|
+
prompt,
|
|
245
|
+
);
|
|
187
246
|
return extractContentText(await ctx.invoke(summarizer, request)).trim();
|
|
188
247
|
}
|
|
189
248
|
|
|
249
|
+
async function summarizeInitial(
|
|
250
|
+
ctx: ExecutorContext,
|
|
251
|
+
summarizer: ExecutorInput,
|
|
252
|
+
request: Program,
|
|
253
|
+
prompt: string,
|
|
254
|
+
): Promise<string> {
|
|
255
|
+
const content = new ProgramView(request).messages
|
|
256
|
+
.filter((message) => message.role === "user")
|
|
257
|
+
.at(-1)?.text.trim();
|
|
258
|
+
if (!content) return "";
|
|
259
|
+
return summarize(ctx, summarizer, prompt, content);
|
|
260
|
+
}
|
|
261
|
+
|
|
190
262
|
function withoutThinking(program: Program): Program {
|
|
191
263
|
const indices = findThinkingBlocks(program).flatMap((block) =>
|
|
192
|
-
Array.from(
|
|
264
|
+
Array.from(
|
|
265
|
+
{ length: block.end - block.start + 1 },
|
|
266
|
+
(_, offset) => block.start + offset,
|
|
267
|
+
),
|
|
193
268
|
);
|
|
194
269
|
return removeInstructions(program, indices);
|
|
195
270
|
}
|
|
@@ -209,25 +284,41 @@ function splitStreamChunk(
|
|
|
209
284
|
chunk: Program,
|
|
210
285
|
terminal: Program[],
|
|
211
286
|
onThinking: (text: string) => void,
|
|
287
|
+
onBody: (text: string) => void,
|
|
212
288
|
): Program {
|
|
213
289
|
const visible = [] as Program["code"];
|
|
214
290
|
const ending = [] as Program["code"];
|
|
215
291
|
for (const instruction of chunk.code) {
|
|
216
|
-
if (
|
|
292
|
+
if (
|
|
293
|
+
instruction.opcode === Opcode.STREAM_THINK_DELTA &&
|
|
294
|
+
instruction.value.kind === "string"
|
|
295
|
+
) {
|
|
217
296
|
onThinking(instruction.value.value);
|
|
218
|
-
} else if (instruction.opcode === Opcode.
|
|
297
|
+
} else if (instruction.opcode === Opcode.STREAM_DELTA) {
|
|
298
|
+
onBody(instruction.value.kind === "string" ? instruction.value.value : "");
|
|
299
|
+
visible.push(instruction);
|
|
300
|
+
} else if (
|
|
301
|
+
instruction.opcode === Opcode.RESP_DONE ||
|
|
302
|
+
instruction.opcode === Opcode.STREAM_END
|
|
303
|
+
) {
|
|
219
304
|
ending.push(instruction);
|
|
220
305
|
} else {
|
|
221
306
|
visible.push(instruction);
|
|
222
307
|
}
|
|
223
308
|
}
|
|
224
|
-
if (ending.length > 0)
|
|
309
|
+
if (ending.length > 0)
|
|
310
|
+
terminal.push({ code: ending, buffers: chunk.buffers });
|
|
225
311
|
return { code: visible, buffers: chunk.buffers };
|
|
226
312
|
}
|
|
227
313
|
|
|
228
314
|
function thinkingChunk(text: string): Program {
|
|
229
315
|
return {
|
|
230
|
-
code: [
|
|
316
|
+
code: [
|
|
317
|
+
{
|
|
318
|
+
opcode: Opcode.STREAM_THINK_DELTA,
|
|
319
|
+
value: { kind: "string", value: `${text} ` },
|
|
320
|
+
},
|
|
321
|
+
],
|
|
231
322
|
buffers: [],
|
|
232
323
|
};
|
|
233
324
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendAssistantMessage,
|
|
3
3
|
appendInstructions,
|
|
4
|
+
appendUserMessage,
|
|
4
5
|
createProgram,
|
|
5
6
|
delta,
|
|
6
7
|
extractContentText,
|
|
@@ -15,7 +16,7 @@ import { createCollapsedReasoner } from "../src/managed/collapsed-reasoner.ts";
|
|
|
15
16
|
import type { Executor, ExecutorContext } from "../src/types.ts";
|
|
16
17
|
|
|
17
18
|
describe("createCollapsedReasoner", () => {
|
|
18
|
-
it("
|
|
19
|
+
it("does not append a summary after completed body content", async () => {
|
|
19
20
|
let summaryRequest: Program | undefined;
|
|
20
21
|
const executor = createCollapsedReasoner(
|
|
21
22
|
fromExecute(async () => withThinking("answer", "private reasoning")),
|
|
@@ -28,32 +29,139 @@ describe("createCollapsedReasoner", () => {
|
|
|
28
29
|
const result = await executor.execute(createProgram(), context());
|
|
29
30
|
|
|
30
31
|
expect(extractContentText(result)).toBe("answer");
|
|
31
|
-
expect(extractThinkingText(result)).toBe("
|
|
32
|
-
expect(
|
|
32
|
+
expect(extractThinkingText(result)).toBe("");
|
|
33
|
+
expect(summaryRequest).toBeUndefined();
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
it("
|
|
36
|
+
it("flushes raw stream reasoning before the end and adds trailing spacing", async () => {
|
|
36
37
|
const executor = createCollapsedReasoner(
|
|
37
38
|
streaming(async function* () {
|
|
38
39
|
yield delta.start();
|
|
39
40
|
yield delta.thinking("private reasoning");
|
|
40
|
-
yield delta.text("answer");
|
|
41
41
|
yield delta.end();
|
|
42
42
|
}),
|
|
43
|
-
fromExecute(async () =>
|
|
43
|
+
fromExecute(async () =>
|
|
44
|
+
appendAssistantMessage(createProgram(), "Checking details."),
|
|
45
|
+
),
|
|
44
46
|
);
|
|
45
47
|
|
|
46
48
|
const chunks: Program[] = [];
|
|
47
|
-
for await (const chunk of executor.stream(createProgram(), context()))
|
|
49
|
+
for await (const chunk of executor.stream(createProgram(), context()))
|
|
50
|
+
chunks.push(chunk);
|
|
48
51
|
|
|
49
|
-
expect(extractStreamThinking(chunks)).toEqual(["Checking details."]);
|
|
50
|
-
expect(chunks.map((chunk) => extractStreamText(chunk)).join("")).toBe("
|
|
52
|
+
expect(extractStreamThinking(chunks)).toEqual(["Checking details. "]);
|
|
53
|
+
expect(chunks.map((chunk) => extractStreamText(chunk)).join("")).toBe("");
|
|
51
54
|
expect(chunks.at(-1)?.code.map((item) => item.opcode)).toEqual([
|
|
52
55
|
Opcode.RESP_DONE,
|
|
53
56
|
Opcode.STREAM_END,
|
|
54
57
|
]);
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
it("suppresses a closed phase and restarts when reasoning resumes", async () => {
|
|
61
|
+
const executor = createCollapsedReasoner(
|
|
62
|
+
streaming(async function* () {
|
|
63
|
+
yield delta.thinking("first reasoning");
|
|
64
|
+
yield delta.text("answer");
|
|
65
|
+
yield delta.thinking("second reasoning");
|
|
66
|
+
yield delta.end();
|
|
67
|
+
}),
|
|
68
|
+
fromExecute(async (request) =>
|
|
69
|
+
appendAssistantMessage(
|
|
70
|
+
createProgram(),
|
|
71
|
+
`Checking ${new ProgramView(request).messages.at(-1)?.text}.`,
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const chunks = await collect(executor.stream(createProgram(), context()));
|
|
77
|
+
|
|
78
|
+
expect(extractStreamThinking(chunks)).toEqual([
|
|
79
|
+
"Checking second reasoning. ",
|
|
80
|
+
]);
|
|
81
|
+
expect(chunks.map((chunk) => extractStreamText(chunk)).join("")).toBe(
|
|
82
|
+
"answer",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("starts an initial update from the user prompt alongside the reasoner", async () => {
|
|
87
|
+
let releaseReasoner: (() => void) | undefined;
|
|
88
|
+
let sawInitialPrompt = false;
|
|
89
|
+
const executor = createCollapsedReasoner(
|
|
90
|
+
streaming(async function* () {
|
|
91
|
+
yield delta.thinking("private reasoning");
|
|
92
|
+
await new Promise<void>((resolve) => {
|
|
93
|
+
releaseReasoner = resolve;
|
|
94
|
+
});
|
|
95
|
+
yield delta.text("answer");
|
|
96
|
+
yield delta.end();
|
|
97
|
+
}),
|
|
98
|
+
fromExecute(async (request) => {
|
|
99
|
+
if (
|
|
100
|
+
new ProgramView(request).messages.at(-1)?.text === "Plan a trip"
|
|
101
|
+
) {
|
|
102
|
+
sawInitialPrompt = true;
|
|
103
|
+
}
|
|
104
|
+
return appendAssistantMessage(createProgram(), "I’m mapping this out.");
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
const iterator = executor
|
|
108
|
+
.stream(appendUserMessage(createProgram(), "Plan a trip"), context())
|
|
109
|
+
[Symbol.asyncIterator]();
|
|
110
|
+
|
|
111
|
+
const first = await iterator.next();
|
|
112
|
+
expect(sawInitialPrompt).toBe(true);
|
|
113
|
+
expect(extractStreamThinking([first.value!])).toEqual([
|
|
114
|
+
"I’m mapping this out. ",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
releaseReasoner?.();
|
|
118
|
+
await collect({
|
|
119
|
+
[Symbol.asyncIterator]: () => iterator,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not delay the answer for an invalidated in-flight summary", async () => {
|
|
124
|
+
vi.useFakeTimers();
|
|
125
|
+
let releaseSummary: (() => void) | undefined;
|
|
126
|
+
let summaryStarted: (() => void) | undefined;
|
|
127
|
+
const executor = createCollapsedReasoner(
|
|
128
|
+
streaming(async function* () {
|
|
129
|
+
yield delta.thinking("private reasoning");
|
|
130
|
+
await new Promise<void>((resolve) => {
|
|
131
|
+
summaryStarted = resolve;
|
|
132
|
+
});
|
|
133
|
+
yield delta.text("answer");
|
|
134
|
+
yield delta.end();
|
|
135
|
+
}),
|
|
136
|
+
fromExecute(async () => {
|
|
137
|
+
summaryStarted?.();
|
|
138
|
+
return new Promise<Program>((resolve) => {
|
|
139
|
+
releaseSummary = () =>
|
|
140
|
+
resolve(appendAssistantMessage(createProgram(), "Late update."));
|
|
141
|
+
});
|
|
142
|
+
}),
|
|
143
|
+
{ interval: 10, forcedInterval: 20 },
|
|
144
|
+
);
|
|
145
|
+
const iterator = executor
|
|
146
|
+
.stream(createProgram(), context())
|
|
147
|
+
[Symbol.asyncIterator]();
|
|
148
|
+
|
|
149
|
+
const answer = iterator.next();
|
|
150
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
151
|
+
const chunk = await answer;
|
|
152
|
+
expect(extractStreamText(chunk.value!)).toBe("answer");
|
|
153
|
+
expect(
|
|
154
|
+
(await iterator.next()).value?.code.some(
|
|
155
|
+
(item: Instruction) => item.opcode === Opcode.STREAM_END,
|
|
156
|
+
),
|
|
157
|
+
).toBe(true);
|
|
158
|
+
expect(await iterator.next()).toMatchObject({ done: true });
|
|
159
|
+
|
|
160
|
+
releaseSummary?.();
|
|
161
|
+
await vi.runAllTimersAsync();
|
|
162
|
+
vi.useRealTimers();
|
|
163
|
+
});
|
|
164
|
+
|
|
57
165
|
it("uses elapsed time for periodic summaries and gives a forced tick priority", async () => {
|
|
58
166
|
vi.useFakeTimers();
|
|
59
167
|
let release: (() => void) | undefined;
|
|
@@ -66,11 +174,16 @@ describe("createCollapsedReasoner", () => {
|
|
|
66
174
|
yield delta.end();
|
|
67
175
|
}),
|
|
68
176
|
fromExecute(async (request) =>
|
|
69
|
-
appendAssistantMessage(
|
|
177
|
+
appendAssistantMessage(
|
|
178
|
+
createProgram(),
|
|
179
|
+
`summary:${extractContentText(request)}`,
|
|
180
|
+
),
|
|
70
181
|
),
|
|
71
182
|
{ interval: 10, forcedInterval: 20 },
|
|
72
183
|
);
|
|
73
|
-
const iterator = executor
|
|
184
|
+
const iterator = executor
|
|
185
|
+
.stream(createProgram(), context())
|
|
186
|
+
[Symbol.asyncIterator]();
|
|
74
187
|
|
|
75
188
|
const pendingSummary = iterator.next();
|
|
76
189
|
await vi.advanceTimersByTimeAsync(20);
|
|
@@ -79,14 +192,25 @@ describe("createCollapsedReasoner", () => {
|
|
|
79
192
|
const end = await iterator.next();
|
|
80
193
|
vi.useRealTimers();
|
|
81
194
|
|
|
82
|
-
expect(extractStreamThinking([summary.value!])).toEqual([
|
|
83
|
-
|
|
195
|
+
expect(extractStreamThinking([summary.value!])).toEqual([
|
|
196
|
+
expect.stringMatching(/first\s$/),
|
|
197
|
+
]);
|
|
198
|
+
expect(
|
|
199
|
+
end.value?.code.some(
|
|
200
|
+
(item: Instruction) => item.opcode === Opcode.STREAM_END,
|
|
201
|
+
),
|
|
202
|
+
).toBe(true);
|
|
84
203
|
});
|
|
85
204
|
|
|
86
205
|
it("validates its timing options and never emits blank summaries", async () => {
|
|
87
|
-
expect(() => createCollapsedReasoner("reasoner", "summarizer", { interval: 0 })).toThrow(RangeError);
|
|
88
206
|
expect(() =>
|
|
89
|
-
createCollapsedReasoner("reasoner", "summarizer", { interval:
|
|
207
|
+
createCollapsedReasoner("reasoner", "summarizer", { interval: 0 }),
|
|
208
|
+
).toThrow(RangeError);
|
|
209
|
+
expect(() =>
|
|
210
|
+
createCollapsedReasoner("reasoner", "summarizer", {
|
|
211
|
+
interval: 10,
|
|
212
|
+
forcedInterval: 9,
|
|
213
|
+
}),
|
|
90
214
|
).toThrow(RangeError);
|
|
91
215
|
|
|
92
216
|
const executor = createCollapsedReasoner(
|
|
@@ -97,7 +221,8 @@ describe("createCollapsedReasoner", () => {
|
|
|
97
221
|
fromExecute(async () => appendAssistantMessage(createProgram(), " ")),
|
|
98
222
|
);
|
|
99
223
|
const chunks: Program[] = [];
|
|
100
|
-
for await (const chunk of executor.stream(createProgram(), context()))
|
|
224
|
+
for await (const chunk of executor.stream(createProgram(), context()))
|
|
225
|
+
chunks.push(chunk);
|
|
101
226
|
expect(extractStreamThinking(chunks)).toEqual([]);
|
|
102
227
|
});
|
|
103
228
|
|
|
@@ -109,7 +234,9 @@ describe("createCollapsedReasoner", () => {
|
|
|
109
234
|
}),
|
|
110
235
|
"summarizer",
|
|
111
236
|
);
|
|
112
|
-
await expect(
|
|
237
|
+
await expect(
|
|
238
|
+
collect(brokenSource.stream(createProgram(), context())),
|
|
239
|
+
).rejects.toBe(sourceFailure);
|
|
113
240
|
|
|
114
241
|
const summaryFailure = new Error("summary failed");
|
|
115
242
|
const brokenSummary = createCollapsedReasoner(
|
|
@@ -121,7 +248,9 @@ describe("createCollapsedReasoner", () => {
|
|
|
121
248
|
throw summaryFailure;
|
|
122
249
|
}),
|
|
123
250
|
);
|
|
124
|
-
await expect(
|
|
251
|
+
await expect(
|
|
252
|
+
collect(brokenSummary.stream(createProgram(), context())),
|
|
253
|
+
).rejects.toBe(summaryFailure);
|
|
125
254
|
});
|
|
126
255
|
});
|
|
127
256
|
|
|
@@ -138,7 +267,11 @@ function withThinking(text: string, thinking: string): Program {
|
|
|
138
267
|
|
|
139
268
|
function context(): ExecutorContext {
|
|
140
269
|
const ctx: ExecutorContext = {
|
|
141
|
-
cache: {
|
|
270
|
+
cache: {
|
|
271
|
+
get: async () => null,
|
|
272
|
+
put: async () => {},
|
|
273
|
+
delete: async () => {},
|
|
274
|
+
},
|
|
142
275
|
requestId: "request",
|
|
143
276
|
executionId: "execution",
|
|
144
277
|
invoke(executor, request) {
|
|
@@ -160,13 +293,19 @@ function fromExecute(execute: Executor["execute"]): Executor {
|
|
|
160
293
|
}
|
|
161
294
|
|
|
162
295
|
function streaming(stream: Executor["stream"]): Executor {
|
|
163
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
async execute() {
|
|
298
|
+
return createProgram();
|
|
299
|
+
},
|
|
300
|
+
stream,
|
|
301
|
+
};
|
|
164
302
|
}
|
|
165
303
|
|
|
166
304
|
function extractStreamText(chunk: Program): string {
|
|
167
305
|
let text = "";
|
|
168
306
|
for (const item of chunk.code) {
|
|
169
|
-
if (item.opcode === Opcode.STREAM_DELTA && item.value.kind === "string")
|
|
307
|
+
if (item.opcode === Opcode.STREAM_DELTA && item.value.kind === "string")
|
|
308
|
+
text += item.value.value;
|
|
170
309
|
}
|
|
171
310
|
return text;
|
|
172
311
|
}
|
|
@@ -175,7 +314,10 @@ function extractStreamThinking(chunks: Program[]): string[] {
|
|
|
175
314
|
const thinking: string[] = [];
|
|
176
315
|
for (const chunk of chunks) {
|
|
177
316
|
for (const item of chunk.code) {
|
|
178
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
item.opcode === Opcode.STREAM_THINK_DELTA &&
|
|
319
|
+
item.value.kind === "string"
|
|
320
|
+
) {
|
|
179
321
|
thinking.push(item.value.value);
|
|
180
322
|
}
|
|
181
323
|
}
|