@neutrome/lilsdk 0.6.3 → 0.6.4
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 +122 -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,6 +20,8 @@ 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 = {
|
|
@@ -52,21 +55,30 @@ export function createCollapsedReasoner(
|
|
|
52
55
|
const response = await ctx.invoke(reasoner, request);
|
|
53
56
|
const reasoning = extractThinkingText(response);
|
|
54
57
|
const concealed = withoutThinking(response);
|
|
55
|
-
if (!reasoning) return concealed;
|
|
58
|
+
if (!reasoning || extractContentText(concealed)) return concealed;
|
|
56
59
|
|
|
57
|
-
const summary = await summarize(
|
|
60
|
+
const summary = await summarize(
|
|
61
|
+
ctx,
|
|
62
|
+
summarizer,
|
|
63
|
+
settings.forcedPrompt,
|
|
64
|
+
reasoning,
|
|
65
|
+
);
|
|
58
66
|
return summary ? appendAssistantThinking(concealed, summary) : concealed;
|
|
59
67
|
},
|
|
60
68
|
|
|
61
69
|
async *stream(request, ctx) {
|
|
62
|
-
const source = ctx
|
|
70
|
+
const source = ctx
|
|
71
|
+
.invokeStream(reasoner, request)
|
|
72
|
+
[Symbol.asyncIterator]();
|
|
63
73
|
let raw = "";
|
|
64
74
|
let startedAt: number | undefined;
|
|
65
75
|
let normalAt = 0;
|
|
66
76
|
let forcedAt = 0;
|
|
67
77
|
let summaries: Promise<void> = Promise.resolve();
|
|
68
78
|
const ready: string[] = [];
|
|
69
|
-
let
|
|
79
|
+
let phase = 0;
|
|
80
|
+
let acceptsSummaries = true;
|
|
81
|
+
let summaryError: { phase: number; error: unknown } | undefined;
|
|
70
82
|
let wake: (() => void) | undefined;
|
|
71
83
|
let wakePromise = nextWake();
|
|
72
84
|
const terminal: Program[] = [];
|
|
@@ -75,16 +87,30 @@ export function createCollapsedReasoner(
|
|
|
75
87
|
wake?.();
|
|
76
88
|
wakePromise = nextWake();
|
|
77
89
|
};
|
|
90
|
+
const initial = summarizeInitial(ctx, summarizer, request).then(
|
|
91
|
+
(summary) => {
|
|
92
|
+
if (summary && acceptsSummaries && phase === 0) ready.push(summary);
|
|
93
|
+
notify();
|
|
94
|
+
},
|
|
95
|
+
(error: unknown) => {
|
|
96
|
+
if (acceptsSummaries && phase === 0)
|
|
97
|
+
summaryError = { phase: 0, error };
|
|
98
|
+
notify();
|
|
99
|
+
},
|
|
100
|
+
);
|
|
78
101
|
const enqueue = (prompt: string, force: boolean) => {
|
|
79
|
-
if (!raw) return;
|
|
102
|
+
if (!acceptsSummaries || !raw) return;
|
|
80
103
|
const interval = raw;
|
|
104
|
+
const summaryPhase = phase;
|
|
81
105
|
raw = "";
|
|
82
106
|
summaries = summaries.then(async () => {
|
|
83
107
|
const summary = await summarize(ctx, summarizer, prompt, interval);
|
|
84
|
-
if (summary
|
|
108
|
+
if (summary && acceptsSummaries && phase === summaryPhase)
|
|
109
|
+
ready.push(summary);
|
|
85
110
|
});
|
|
86
111
|
void summaries.then(notify, (error: unknown) => {
|
|
87
|
-
|
|
112
|
+
if (acceptsSummaries && phase === summaryPhase)
|
|
113
|
+
summaryError = { phase: summaryPhase, error };
|
|
88
114
|
notify();
|
|
89
115
|
});
|
|
90
116
|
if (force) normalAt = Date.now();
|
|
@@ -93,11 +119,12 @@ export function createCollapsedReasoner(
|
|
|
93
119
|
let next = source.next();
|
|
94
120
|
try {
|
|
95
121
|
for (;;) {
|
|
96
|
-
if (summaryError) throw summaryError;
|
|
97
|
-
while (ready.length > 0)
|
|
122
|
+
if (summaryError?.phase === phase) throw summaryError.error;
|
|
123
|
+
while (acceptsSummaries && ready.length > 0)
|
|
124
|
+
yield thinkingChunk(ready.shift()!);
|
|
98
125
|
|
|
99
126
|
const now = Date.now();
|
|
100
|
-
if (startedAt !== undefined) {
|
|
127
|
+
if (acceptsSummaries && startedAt !== undefined) {
|
|
101
128
|
if (now >= forcedAt) {
|
|
102
129
|
enqueue(settings.forcedPrompt, true);
|
|
103
130
|
forcedAt = now + settings.forcedInterval;
|
|
@@ -111,34 +138,55 @@ export function createCollapsedReasoner(
|
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
|
|
114
|
-
const due =
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
const due =
|
|
142
|
+
!acceptsSummaries || startedAt === undefined
|
|
143
|
+
? undefined
|
|
144
|
+
: Math.max(0, Math.min(normalAt, forcedAt) - Date.now());
|
|
117
145
|
const event = await Promise.race([
|
|
118
146
|
next.then((result) => ({ type: "source" as const, result })),
|
|
119
147
|
wakePromise.then(() => ({ type: "summary" as const })),
|
|
120
|
-
...(due === undefined
|
|
148
|
+
...(due === undefined
|
|
149
|
+
? []
|
|
150
|
+
: [delay(due).then(() => ({ type: "timer" as const }))]),
|
|
121
151
|
]);
|
|
122
152
|
if (event.type !== "source") continue;
|
|
123
153
|
next = source.next();
|
|
124
154
|
if (event.result.done) break;
|
|
125
155
|
|
|
126
|
-
const visible = splitStreamChunk(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
const visible = splitStreamChunk(
|
|
157
|
+
event.result.value,
|
|
158
|
+
terminal,
|
|
159
|
+
(text) => {
|
|
160
|
+
if (!text) return;
|
|
161
|
+
if (!acceptsSummaries) acceptsSummaries = true;
|
|
162
|
+
raw += text;
|
|
163
|
+
if (startedAt === undefined) {
|
|
164
|
+
const timestamp = Date.now();
|
|
165
|
+
startedAt = timestamp;
|
|
166
|
+
normalAt = timestamp + settings.interval;
|
|
167
|
+
forcedAt = timestamp + settings.forcedInterval;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
(text) => {
|
|
171
|
+
if (!text || !acceptsSummaries) return;
|
|
172
|
+
acceptsSummaries = false;
|
|
173
|
+
phase += 1;
|
|
174
|
+
raw = "";
|
|
175
|
+
startedAt = undefined;
|
|
176
|
+
ready.length = 0;
|
|
177
|
+
summaries = Promise.resolve();
|
|
178
|
+
},
|
|
179
|
+
);
|
|
135
180
|
if (visible.code.length > 0) yield visible;
|
|
136
181
|
}
|
|
137
182
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
183
|
+
if (acceptsSummaries) {
|
|
184
|
+
enqueue(settings.forcedPrompt, true);
|
|
185
|
+
await Promise.all([summaries, ...(phase === 0 ? [initial] : [])]);
|
|
186
|
+
}
|
|
187
|
+
if (summaryError?.phase === phase) throw summaryError.error;
|
|
188
|
+
if (acceptsSummaries)
|
|
189
|
+
while (ready.length > 0) yield thinkingChunk(ready.shift()!);
|
|
142
190
|
yield* terminal;
|
|
143
191
|
} finally {
|
|
144
192
|
await source.return?.();
|
|
@@ -154,13 +202,18 @@ export function createCollapsedReasoner(
|
|
|
154
202
|
}
|
|
155
203
|
|
|
156
204
|
function parseOptions(options: CollapsedReasonerOptions) {
|
|
157
|
-
const interval = positiveDuration(
|
|
205
|
+
const interval = positiveDuration(
|
|
206
|
+
options.interval ?? DEFAULT_INTERVAL,
|
|
207
|
+
"interval",
|
|
208
|
+
);
|
|
158
209
|
const forcedInterval = positiveDuration(
|
|
159
210
|
options.forcedInterval ?? DEFAULT_FORCED_INTERVAL,
|
|
160
211
|
"forcedInterval",
|
|
161
212
|
);
|
|
162
213
|
if (forcedInterval < interval) {
|
|
163
|
-
throw new RangeError(
|
|
214
|
+
throw new RangeError(
|
|
215
|
+
"forcedInterval must be greater than or equal to interval",
|
|
216
|
+
);
|
|
164
217
|
}
|
|
165
218
|
return {
|
|
166
219
|
interval,
|
|
@@ -183,13 +236,31 @@ async function summarize(
|
|
|
183
236
|
prompt: string,
|
|
184
237
|
reasoning: string,
|
|
185
238
|
): Promise<string> {
|
|
186
|
-
const request = prependSystemPrompt(
|
|
239
|
+
const request = prependSystemPrompt(
|
|
240
|
+
appendUserMessage(createProgram(), reasoning),
|
|
241
|
+
prompt,
|
|
242
|
+
);
|
|
187
243
|
return extractContentText(await ctx.invoke(summarizer, request)).trim();
|
|
188
244
|
}
|
|
189
245
|
|
|
246
|
+
async function summarizeInitial(
|
|
247
|
+
ctx: ExecutorContext,
|
|
248
|
+
summarizer: ExecutorInput,
|
|
249
|
+
request: Program,
|
|
250
|
+
): Promise<string> {
|
|
251
|
+
const prompt = new ProgramView(request).messages
|
|
252
|
+
.filter((message) => message.role === "user")
|
|
253
|
+
.at(-1)?.text.trim();
|
|
254
|
+
if (!prompt) return "";
|
|
255
|
+
return summarize(ctx, summarizer, INITIAL_PROMPT, prompt);
|
|
256
|
+
}
|
|
257
|
+
|
|
190
258
|
function withoutThinking(program: Program): Program {
|
|
191
259
|
const indices = findThinkingBlocks(program).flatMap((block) =>
|
|
192
|
-
Array.from(
|
|
260
|
+
Array.from(
|
|
261
|
+
{ length: block.end - block.start + 1 },
|
|
262
|
+
(_, offset) => block.start + offset,
|
|
263
|
+
),
|
|
193
264
|
);
|
|
194
265
|
return removeInstructions(program, indices);
|
|
195
266
|
}
|
|
@@ -209,25 +280,41 @@ function splitStreamChunk(
|
|
|
209
280
|
chunk: Program,
|
|
210
281
|
terminal: Program[],
|
|
211
282
|
onThinking: (text: string) => void,
|
|
283
|
+
onBody: (text: string) => void,
|
|
212
284
|
): Program {
|
|
213
285
|
const visible = [] as Program["code"];
|
|
214
286
|
const ending = [] as Program["code"];
|
|
215
287
|
for (const instruction of chunk.code) {
|
|
216
|
-
if (
|
|
288
|
+
if (
|
|
289
|
+
instruction.opcode === Opcode.STREAM_THINK_DELTA &&
|
|
290
|
+
instruction.value.kind === "string"
|
|
291
|
+
) {
|
|
217
292
|
onThinking(instruction.value.value);
|
|
218
|
-
} else if (instruction.opcode === Opcode.
|
|
293
|
+
} else if (instruction.opcode === Opcode.STREAM_DELTA) {
|
|
294
|
+
onBody(instruction.value.kind === "string" ? instruction.value.value : "");
|
|
295
|
+
visible.push(instruction);
|
|
296
|
+
} else if (
|
|
297
|
+
instruction.opcode === Opcode.RESP_DONE ||
|
|
298
|
+
instruction.opcode === Opcode.STREAM_END
|
|
299
|
+
) {
|
|
219
300
|
ending.push(instruction);
|
|
220
301
|
} else {
|
|
221
302
|
visible.push(instruction);
|
|
222
303
|
}
|
|
223
304
|
}
|
|
224
|
-
if (ending.length > 0)
|
|
305
|
+
if (ending.length > 0)
|
|
306
|
+
terminal.push({ code: ending, buffers: chunk.buffers });
|
|
225
307
|
return { code: visible, buffers: chunk.buffers };
|
|
226
308
|
}
|
|
227
309
|
|
|
228
310
|
function thinkingChunk(text: string): Program {
|
|
229
311
|
return {
|
|
230
|
-
code: [
|
|
312
|
+
code: [
|
|
313
|
+
{
|
|
314
|
+
opcode: Opcode.STREAM_THINK_DELTA,
|
|
315
|
+
value: { kind: "string", value: `${text} ` },
|
|
316
|
+
},
|
|
317
|
+
],
|
|
231
318
|
buffers: [],
|
|
232
319
|
};
|
|
233
320
|
}
|
|
@@ -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
|
}
|