@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neutrome/lilsdk",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
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,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(ctx, summarizer, settings.forcedPrompt, reasoning);
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.invokeStream(reasoner, request)[Symbol.asyncIterator]();
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 summaryError: unknown;
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) ready.push(summary);
108
+ if (summary && acceptsSummaries && phase === summaryPhase)
109
+ ready.push(summary);
85
110
  });
86
111
  void summaries.then(notify, (error: unknown) => {
87
- summaryError = error;
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) yield thinkingChunk(ready.shift()!);
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 = startedAt === undefined
115
- ? undefined
116
- : Math.max(0, Math.min(normalAt, forcedAt) - Date.now());
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 ? [] : [delay(due).then(() => ({ type: "timer" as const }))]),
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(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
- });
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
- enqueue(settings.forcedPrompt, true);
139
- await summaries;
140
- if (summaryError) throw summaryError;
141
- while (ready.length > 0) yield thinkingChunk(ready.shift()!);
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(options.interval ?? DEFAULT_INTERVAL, "interval");
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("forcedInterval must be greater than or equal to interval");
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(appendUserMessage(createProgram(), reasoning), prompt);
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({ length: block.end - block.start + 1 }, (_, offset) => block.start + offset),
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 (instruction.opcode === Opcode.STREAM_THINK_DELTA && instruction.value.kind === "string") {
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.RESP_DONE || instruction.opcode === Opcode.STREAM_END) {
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) terminal.push({ code: ending, buffers: chunk.buffers });
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: [{ opcode: Opcode.STREAM_THINK_DELTA, value: { kind: "string", value: text } }],
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("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
  }