@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neutrome/lilsdk",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -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(ctx, summarizer, settings.forcedPrompt, reasoning);
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.invokeStream(reasoner, request)[Symbol.asyncIterator]();
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 summaryError: unknown;
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) ready.push(summary);
110
+ if (summary && acceptsSummaries && phase === summaryPhase)
111
+ ready.push(summary);
85
112
  });
86
113
  void summaries.then(notify, (error: unknown) => {
87
- summaryError = error;
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) yield thinkingChunk(ready.shift()!);
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 = startedAt === undefined
115
- ? undefined
116
- : Math.max(0, Math.min(normalAt, forcedAt) - Date.now());
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 ? [] : [delay(due).then(() => ({ type: "timer" as const }))]),
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(event.result.value, terminal, (text) => {
127
- raw += text;
128
- if (startedAt === undefined) {
129
- const timestamp = Date.now();
130
- startedAt = timestamp;
131
- normalAt = timestamp + settings.interval;
132
- forcedAt = timestamp + settings.forcedInterval;
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
- enqueue(settings.forcedPrompt, true);
139
- await summaries;
140
- if (summaryError) throw summaryError;
141
- while (ready.length > 0) yield thinkingChunk(ready.shift()!);
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(options.interval ?? DEFAULT_INTERVAL, "interval");
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("forcedInterval must be greater than or equal to interval");
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(appendUserMessage(createProgram(), reasoning), prompt);
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({ length: block.end - block.start + 1 }, (_, offset) => block.start + offset),
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 (instruction.opcode === Opcode.STREAM_THINK_DELTA && instruction.value.kind === "string") {
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.RESP_DONE || instruction.opcode === Opcode.STREAM_END) {
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) terminal.push({ code: ending, buffers: chunk.buffers });
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: [{ opcode: Opcode.STREAM_THINK_DELTA, value: { kind: "string", value: text } }],
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("replaces completed raw reasoning with a forced summary", async () => {
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("Working through it.");
32
- expect(new ProgramView(summaryRequest!).messages.at(-1)?.text).toBe("private reasoning");
32
+ expect(extractThinkingText(result)).toBe("");
33
+ expect(summaryRequest).toBeUndefined();
33
34
  });
34
35
 
35
- it("suppresses raw stream deltas, preserves visible chunks, and flushes before the end", async () => {
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 () => appendAssistantMessage(createProgram(), "Checking details.")),
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())) chunks.push(chunk);
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("answer");
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(createProgram(), `summary:${extractContentText(request)}`),
177
+ appendAssistantMessage(
178
+ createProgram(),
179
+ `summary:${extractContentText(request)}`,
180
+ ),
70
181
  ),
71
182
  { interval: 10, forcedInterval: 20 },
72
183
  );
73
- const iterator = executor.stream(createProgram(), context())[Symbol.asyncIterator]();
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([expect.stringContaining("first")]);
83
- expect(end.value?.code.some((item: Instruction) => item.opcode === Opcode.STREAM_END)).toBe(true);
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: 10, forcedInterval: 9 }),
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())) chunks.push(chunk);
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(collect(brokenSource.stream(createProgram(), context()))).rejects.toBe(sourceFailure);
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(collect(brokenSummary.stream(createProgram(), context()))).rejects.toBe(summaryFailure);
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: { get: async () => null, put: async () => {}, delete: async () => {} },
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 { async execute() { return createProgram(); }, stream };
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") text += item.value.value;
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 (item.opcode === Opcode.STREAM_THINK_DELTA && item.value.kind === "string") {
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
  }