@neutrome/lilsdk 0.4.6 → 0.5.0

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/src/index.ts CHANGED
@@ -1,15 +1,41 @@
1
- export type {
2
- Cache,
3
- CachePutOptions,
4
- Executor,
5
- ExecutorInput,
6
- ExecutorContext,
7
- OutputSink,
8
- ProgramTransform,
9
- ExecutionEvent,
10
- ExecutionEventInput,
11
- Tool,
12
- TransformCapability,
13
- TransformContext,
14
- } from "./types.ts";
15
- export { createExecutionEvent } from "./types.ts";
1
+ /**
2
+ * Runtime contracts and combinators for building executors.
3
+ *
4
+ * The root entry point carries only contracts — {@link Executor},
5
+ * {@link Tool}, {@link ProgramTransform}, {@link ExecutorContext} and friends.
6
+ * Everything that does work lives on a subpath, and each symbol has exactly one
7
+ * import path:
8
+ *
9
+ * - `@neutrome/lilsdk/loops` — `retry`, `fallback`, goal loops
10
+ * - `@neutrome/lilsdk/tools` — tool loops
11
+ * - `@neutrome/lilsdk/managed` — capability and attachment executors
12
+ * - `@neutrome/lilsdk/stream` — stream observation and output helpers
13
+ * - `@neutrome/lilsdk/primitives` — functional helpers over instruction lists
14
+ *
15
+ * Programs themselves are built with `@neutrome/lil-engine`, which owns the
16
+ * protocol; this package owns everything that runs.
17
+ *
18
+ * Executor factories are named `create*Executor`, except `retry` and `fallback`,
19
+ * which read better as bare verbs.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import type { Executor } from "@neutrome/lilsdk";
24
+ * import { fallback, retry } from "@neutrome/lilsdk/loops";
25
+ * import { prependSystemPrompt } from "@neutrome/lil-engine";
26
+ *
27
+ * const upstream = fallback([retry("default/glm-5.2", { attempts: 3 }), "fast/gemma-4-31b"]);
28
+ *
29
+ * const executor: Executor = {
30
+ * async execute(request, ctx) {
31
+ * return ctx.invoke(upstream, prependSystemPrompt(request, "Be concise."));
32
+ * },
33
+ * async *stream(request, ctx) {
34
+ * yield* ctx.invokeStream(upstream, prependSystemPrompt(request, "Be concise."));
35
+ * },
36
+ * };
37
+ * ```
38
+ *
39
+ * @packageDocumentation
40
+ */
41
+ export * from "./types.ts";
@@ -0,0 +1,111 @@
1
+ import {
2
+ appendAssistantMessage,
3
+ createProgram,
4
+ delta,
5
+ type Program,
6
+ } from "@neutrome/lil-engine";
7
+ import type { Executor, ExecutorInput } from "../types.ts";
8
+ import { streamStage } from "../stream/index.ts";
9
+ import { childContext, positiveInteger } from "./shared.ts";
10
+
11
+ /**
12
+ * Options accepted by {@link createGoalExecutor}.
13
+ */
14
+ export type GoalExecutorOptions = {
15
+ draft: ExecutorInput;
16
+ review: ExecutorInput;
17
+ refine: (
18
+ request: Program,
19
+ answer: Program,
20
+ review: Program,
21
+ attempt: number,
22
+ ) => Program | Promise<Program>;
23
+ satisfied: (review: Program, attempt: number) => boolean | Promise<boolean>;
24
+ maxIterations?: number;
25
+ };
26
+
27
+ /**
28
+ * Builds an executor that drafts, reviews and refines until a goal is met.
29
+ *
30
+ * @param options - Draft and review executors, plus the refine and satisfied predicates.
31
+ * @returns An executor that iterates up to `maxIterations` times.
32
+ * @example
33
+ * ```ts
34
+ * export default createGoalExecutor({
35
+ * draft: "core/turn-1",
36
+ * review: "core/critic-1",
37
+ * satisfied: (review) => extractContentText(review).includes("APPROVED"),
38
+ * refine: (request, answer, review) => appendUserMessage(request, extractContentText(review)),
39
+ * });
40
+ * ```
41
+ */
42
+ export function createGoalExecutor(options: GoalExecutorOptions): Executor {
43
+ const maxIterations = positiveInteger(
44
+ options.maxIterations ?? 3,
45
+ "goal maxIterations",
46
+ );
47
+
48
+ return {
49
+ async execute(request, ctx) {
50
+ let current = request;
51
+ let answer = request;
52
+
53
+ for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
54
+ const attemptContext = childContext(ctx, attempt);
55
+ answer = await attemptContext.invoke(options.draft, current);
56
+ const review = await attemptContext.invoke(options.review, answer);
57
+ if (await options.satisfied(review, attempt)) return answer;
58
+ if (attempt < maxIterations) {
59
+ current = await options.refine(current, answer, review, attempt);
60
+ }
61
+ }
62
+
63
+ return answer;
64
+ },
65
+
66
+ async *stream(request, ctx) {
67
+ let current = request;
68
+ let answer = request;
69
+
70
+ for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
71
+ const attemptContext = childContext(ctx, attempt);
72
+ answer = yield* streamGoalStage(
73
+ streamStage(attemptContext.invokeStream(options.draft, current)),
74
+ current,
75
+ );
76
+ const review = yield* streamGoalStage(
77
+ streamStage(attemptContext.invokeStream(options.review, answer)),
78
+ answer,
79
+ );
80
+
81
+ if (await options.satisfied(review, attempt)) {
82
+ yield delta.end();
83
+ return;
84
+ }
85
+ if (attempt < maxIterations) {
86
+ current = await options.refine(current, answer, review, attempt);
87
+ }
88
+ }
89
+
90
+ yield delta.end();
91
+ },
92
+ };
93
+ }
94
+
95
+ async function* streamGoalStage(
96
+ source: AsyncIterable<Program>,
97
+ fallback: Program,
98
+ ): AsyncGenerator<Program, Program> {
99
+ let final = fallback;
100
+ let transcript = "";
101
+
102
+ for await (const chunk of source) {
103
+ final = chunk;
104
+ transcript += delta.extractContentText(chunk);
105
+ yield chunk;
106
+ }
107
+
108
+ return transcript
109
+ ? appendAssistantMessage(createProgram(), transcript)
110
+ : final;
111
+ }
@@ -1,14 +1,21 @@
1
- import { createProgram, deltaText, type Program } from "@neutrome/lil-engine";
2
1
  import type { Executor, ExecutorContext, ExecutorInput } from "../types.ts";
3
- import { completeStream, streamStage } from "../stream/stages.ts";
4
- import { appendAssistantMessage } from "../synthetic/index.ts";
2
+ import { childContext, positiveInteger, shouldContinue } from "./shared.ts";
5
3
 
4
+ export { createGoalExecutor } from "./goal.ts";
5
+ export type { GoalExecutorOptions } from "./goal.ts";
6
+
7
+ /**
8
+ * Options accepted by {@link retry}.
9
+ */
6
10
  export type RetryOptions = {
7
11
  attempts?: number;
8
12
  shouldRetry?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
9
13
  onRetry?: (error: unknown, attempt: number) => void | Promise<void>;
10
14
  };
11
15
 
16
+ /**
17
+ * Options accepted by {@link fallback}.
18
+ */
12
19
  export type FallbackOptions = {
13
20
  shouldFallback?: (
14
21
  error: unknown,
@@ -17,19 +24,20 @@ export type FallbackOptions = {
17
24
  onFallback?: (error: unknown, executorIndex: number) => void | Promise<void>;
18
25
  };
19
26
 
20
- export type GoalExecutorOptions = {
21
- draft: ExecutorInput;
22
- review: ExecutorInput;
23
- refine: (
24
- request: Program,
25
- answer: Program,
26
- review: Program,
27
- attempt: number,
28
- ) => Program | Promise<Program>;
29
- satisfied: (review: Program, attempt: number) => boolean | Promise<boolean>;
30
- maxIterations?: number;
31
- };
32
-
27
+ /**
28
+ * Retries an executor until it succeeds or runs out of attempts.
29
+ *
30
+ * A streaming attempt that has already emitted a chunk is never retried, so a
31
+ * client never sees the same text twice.
32
+ *
33
+ * @param executor - Executor or model id to run.
34
+ * @param options - Attempt count and retry predicates.
35
+ * @returns An executor that retries on failure.
36
+ * @example
37
+ * ```ts
38
+ * const upstream = retry("default/glm-5.2", { attempts: 3 });
39
+ * ```
40
+ */
33
41
  export function retry(
34
42
  executor: ExecutorInput,
35
43
  options: RetryOptions = {},
@@ -77,6 +85,23 @@ export function retry(
77
85
  };
78
86
  }
79
87
 
88
+ /**
89
+ * Tries executors in order until one succeeds.
90
+ *
91
+ * A streaming attempt that has already emitted a chunk is never abandoned.
92
+ *
93
+ * @param executors - Executors or model ids to try, best first.
94
+ * @param options - Fallback predicates and hooks.
95
+ * @returns An executor that fails over.
96
+ * @throws Error when `executors` is empty.
97
+ * @example
98
+ * ```ts
99
+ * const upstream = fallback([
100
+ * retry("default/glm-5.2", { attempts: 3 }),
101
+ * "fast/gemma-4-31b",
102
+ * ]);
103
+ * ```
104
+ */
80
105
  export function fallback(
81
106
  executors: readonly ExecutorInput[],
82
107
  options: FallbackOptions = {},
@@ -127,77 +152,6 @@ export function fallback(
127
152
  };
128
153
  }
129
154
 
130
- export function createGoalExecutor(options: GoalExecutorOptions): Executor {
131
- const maxIterations = positiveInteger(
132
- options.maxIterations ?? 3,
133
- "goal maxIterations",
134
- );
135
-
136
- return {
137
- async execute(request, ctx) {
138
- let current = request;
139
- let answer = request;
140
-
141
- for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
142
- const attemptContext = childContext(ctx, attempt);
143
- answer = await attemptContext.invoke(options.draft, current);
144
- const review = await attemptContext.invoke(options.review, answer);
145
- if (await options.satisfied(review, attempt)) return answer;
146
- if (attempt < maxIterations) {
147
- current = await options.refine(current, answer, review, attempt);
148
- }
149
- }
150
-
151
- return answer;
152
- },
153
-
154
- async *stream(request, ctx) {
155
- let current = request;
156
- let answer = request;
157
-
158
- for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
159
- const attemptContext = childContext(ctx, attempt);
160
- answer = yield* streamGoalStage(
161
- streamStage(attemptContext.invokeStream(options.draft, current)),
162
- current,
163
- );
164
- const review = yield* streamGoalStage(
165
- streamStage(attemptContext.invokeStream(options.review, answer)),
166
- answer,
167
- );
168
-
169
- if (await options.satisfied(review, attempt)) {
170
- yield completeStream();
171
- return;
172
- }
173
- if (attempt < maxIterations) {
174
- current = await options.refine(current, answer, review, attempt);
175
- }
176
- }
177
-
178
- yield completeStream();
179
- },
180
- };
181
- }
182
-
183
- async function* streamGoalStage(
184
- source: AsyncIterable<Program>,
185
- fallback: Program,
186
- ): AsyncGenerator<Program, Program> {
187
- let final = fallback;
188
- let transcript = "";
189
-
190
- for await (const chunk of source) {
191
- final = chunk;
192
- transcript += deltaText(chunk);
193
- yield chunk;
194
- }
195
-
196
- return transcript
197
- ? appendAssistantMessage(createProgram(), transcript)
198
- : final;
199
- }
200
-
201
155
  async function runRetry<T>(
202
156
  operation: (attempt: number) => Promise<T>,
203
157
  attempts: number,
@@ -247,26 +201,3 @@ async function runFallback<T>(
247
201
 
248
202
  throw lastError;
249
203
  }
250
-
251
- async function shouldContinue(
252
- error: unknown,
253
- attempt: number,
254
- predicate?: (error: unknown, attempt: number) => boolean | Promise<boolean>,
255
- ): Promise<boolean> {
256
- return predicate ? predicate(error, attempt) : true;
257
- }
258
-
259
- function childContext(ctx: ExecutorContext, attempt: number): ExecutorContext {
260
- return {
261
- ...ctx,
262
- executionId: `${ctx.executionId}:${attempt}`,
263
- parentExecutionId: ctx.executionId,
264
- };
265
- }
266
-
267
- function positiveInteger(value: number, label: string): number {
268
- if (!Number.isInteger(value) || value < 1) {
269
- throw new Error(`${label} must be a positive integer`);
270
- }
271
- return value;
272
- }
@@ -0,0 +1,27 @@
1
+ import type { ExecutorContext } from "../types.ts";
2
+
3
+ export async function shouldContinue(
4
+ error: unknown,
5
+ attempt: number,
6
+ predicate?: (error: unknown, attempt: number) => boolean | Promise<boolean>,
7
+ ): Promise<boolean> {
8
+ return predicate ? predicate(error, attempt) : true;
9
+ }
10
+
11
+ export function childContext(
12
+ ctx: ExecutorContext,
13
+ attempt: number,
14
+ ): ExecutorContext {
15
+ return {
16
+ ...ctx,
17
+ executionId: `${ctx.executionId}:${attempt}`,
18
+ parentExecutionId: ctx.executionId,
19
+ };
20
+ }
21
+
22
+ export function positiveInteger(value: number, label: string): number {
23
+ if (!Number.isInteger(value) || value < 1) {
24
+ throw new Error(`${label} must be a positive integer`);
25
+ }
26
+ return value;
27
+ }
@@ -1,32 +1,54 @@
1
1
  import {
2
- contentText,
2
+ appendToolInteraction,
3
+ extractAttachments,
4
+ extractContentText,
5
+ findMessages,
3
6
  insertAfter,
4
7
  isAttachmentOpcode,
5
- messages,
6
- programAttachments,
8
+ prependSystemPrompt,
7
9
  type Program,
8
10
  type ProgramAttachment,
9
11
  } from "@neutrome/lil-engine";
10
- import {
11
- appendToolInteraction,
12
- prependSystemPrompt,
13
- } from "../synthetic/index.ts";
14
12
  import type { Executor, ExecutorContext, ExecutorInput } from "../types.ts";
15
13
 
16
14
  const toolName = "attachment_to_text";
17
15
  const defaultPrompt =
18
16
  "Read the attached file carefully. Return a detailed factual description of its content for another assistant to use. Include all relevant visible text, structure, and details. Do not discuss this instruction.";
19
17
 
18
+ /**
19
+ * Maps a set of MIME types to the executor that can read them.
20
+ */
20
21
  export type AttachmentReaderRule = {
21
22
  mimeTypes: readonly string[];
22
23
  executor: ExecutorInput;
23
24
  };
24
25
 
26
+ /**
27
+ * Options accepted by {@link createAttachmentToTextExecutor}.
28
+ */
25
29
  export type AttachmentToTextOptions = {
26
30
  systemPrompt?: string;
27
31
  cacheKey?: (attachment: ProgramAttachment, ctx: ExecutorContext) => string;
28
32
  };
29
33
 
34
+ /**
35
+ * Wraps a text-only executor so it can answer requests carrying attachments.
36
+ *
37
+ * Each attachment is handed to the first rule matching its MIME type, and the
38
+ * description that comes back replaces the attachment in the request.
39
+ *
40
+ * @param inner - Text-only executor or model id.
41
+ * @param rules - Reader executors, in priority order. `"*"` matches any type.
42
+ * @param options - Reader prompt and cache key.
43
+ * @returns An executor that accepts attachments.
44
+ * @throws Error when `rules` is empty.
45
+ * @example
46
+ * ```ts
47
+ * export default createAttachmentToTextExecutor("default/glm-5.2", [
48
+ * { mimeTypes: ["*"], executor: "default/gemma-4-31b" },
49
+ * ]);
50
+ * ```
51
+ */
30
52
  export function createAttachmentToTextExecutor(
31
53
  inner: ExecutorInput,
32
54
  rules: readonly AttachmentReaderRule[],
@@ -59,13 +81,13 @@ async function prepareRequest(
59
81
  prompt: string,
60
82
  options: AttachmentToTextOptions,
61
83
  ): Promise<Program> {
62
- const attachments = programAttachments(request);
84
+ const attachments = extractAttachments(request);
63
85
  if (attachments.length === 0) return request;
64
86
  const latestUserStart = [...attachments]
65
87
  .filter((attachment) => attachment.message.role === "user")
66
88
  .at(-1)?.message.start;
67
89
  let result = stripAttachments(request);
68
- const sourceMessages = messages(request);
90
+ const sourceMessages = findMessages(request);
69
91
 
70
92
  for (const attachment of [...attachments].reverse()) {
71
93
  const rule = rules.find((candidate) =>
@@ -90,7 +112,9 @@ async function prepareRequest(
90
112
  );
91
113
  }
92
114
  const readerRequest = readerProgram(request, attachment, prompt);
93
- description = contentText(await ctx.invoke(rule.executor, readerRequest));
115
+ description = extractContentText(
116
+ await ctx.invoke(rule.executor, readerRequest),
117
+ );
94
118
  if (!description.trim())
95
119
  throw new Error(
96
120
  `Attachment reader returned no text for ${attachment.mimeType}`,
@@ -111,7 +135,7 @@ async function prepareRequest(
111
135
  const messageIndex = sourceMessages.findIndex(
112
136
  (message) => message.start === attachment.message.start,
113
137
  );
114
- const targetMessage = messages(result)[messageIndex];
138
+ const targetMessage = findMessages(result)[messageIndex];
115
139
  if (!targetMessage)
116
140
  throw new Error("Attachment message was removed from context");
117
141
  const synthetic = appendToolInteraction(
@@ -1,12 +1,12 @@
1
1
  import {
2
- addTool,
3
- clearIndices,
4
- toolDefinitions,
5
- viewProgram,
2
+ appendTool,
3
+ findToolDefinitions,
4
+ ProgramView,
5
+ removeInstructions,
6
6
  type Program,
7
7
  type ProgramTool,
8
8
  } from "@neutrome/lil-engine";
9
- import { connectTools } from "../tools.ts";
9
+ import { createToolsExecutor } from "../tools.ts";
10
10
  import type {
11
11
  Executor,
12
12
  ExecutorContext,
@@ -17,6 +17,9 @@ import type {
17
17
  const capabilityToolName = "learn_capability";
18
18
  const encoder = new TextEncoder();
19
19
 
20
+ /**
21
+ * Options accepted by {@link createCapabilitiesExecutor}.
22
+ */
20
23
  export type CapabilitiesExecutorOptions = {
21
24
  enabledIterations?: number;
22
25
  cacheKey?: (ctx: ExecutorContext) => string;
@@ -25,6 +28,20 @@ export type CapabilitiesExecutorOptions = {
25
28
 
26
29
  type CapabilitySelection = { toolName: string; remaining: number };
27
30
 
31
+ /**
32
+ * Wraps an executor so the model can look up its own capabilities on demand.
33
+ *
34
+ * A `learn_capability` tool is offered for the first few iterations; what the
35
+ * model learns is cached per request, so repeated lookups are free.
36
+ *
37
+ * @param inner - Executor or model id that does the generating.
38
+ * @param options - Iteration budget, cache key and prompt prefixes to skip.
39
+ * @returns An executor that can answer questions about itself.
40
+ * @example
41
+ * ```ts
42
+ * export default createCapabilitiesExecutor("core/voice-1");
43
+ * ```
44
+ */
28
45
  export function createCapabilitiesExecutor(
29
46
  inner: ExecutorInput,
30
47
  options: CapabilitiesExecutorOptions = {},
@@ -35,7 +52,7 @@ export function createCapabilitiesExecutor(
35
52
 
36
53
  return {
37
54
  async execute(request, ctx) {
38
- const tools = viewProgram(request).tools;
55
+ const tools = new ProgramView(request).tools;
39
56
  if (tools.length === 0) return ctx.invoke(inner, request);
40
57
 
41
58
  const managedTools = tools.filter(
@@ -45,14 +62,16 @@ export function createCapabilitiesExecutor(
45
62
 
46
63
  const key = selectionKey(cacheKey(ctx));
47
64
  await discardMissingSelection(ctx, key, managedTools);
48
- return connectCapabilities(inner, managedTools, key, enabledIterations).execute(
49
- withoutManagedTools(request, managedTools),
50
- ctx,
51
- );
65
+ return connectCapabilities(
66
+ inner,
67
+ managedTools,
68
+ key,
69
+ enabledIterations,
70
+ ).execute(withoutManagedTools(request, managedTools), ctx);
52
71
  },
53
72
 
54
73
  async *stream(request, ctx) {
55
- const tools = viewProgram(request).tools;
74
+ const tools = new ProgramView(request).tools;
56
75
  if (tools.length === 0) {
57
76
  yield* ctx.invokeStream(inner, request);
58
77
  return;
@@ -68,10 +87,12 @@ export function createCapabilitiesExecutor(
68
87
 
69
88
  const key = selectionKey(cacheKey(ctx));
70
89
  await discardMissingSelection(ctx, key, managedTools);
71
- yield* connectCapabilities(inner, managedTools, key, enabledIterations).stream(
72
- withoutManagedTools(request, managedTools),
73
- ctx,
74
- );
90
+ yield* connectCapabilities(
91
+ inner,
92
+ managedTools,
93
+ key,
94
+ enabledIterations,
95
+ ).stream(withoutManagedTools(request, managedTools), ctx);
75
96
  },
76
97
  };
77
98
  }
@@ -97,10 +118,9 @@ function connectCapabilities(
97
118
  },
98
119
  };
99
120
 
100
- return connectTools(
101
- [learnCapabilityTool(tools, key, enabledIterations)],
102
- selectedInner,
103
- );
121
+ return createToolsExecutor(selectedInner, [
122
+ learnCapabilityTool(tools, key, enabledIterations),
123
+ ]);
104
124
  }
105
125
 
106
126
  function learnCapabilityTool(
@@ -174,17 +194,17 @@ function withoutManagedTools(
174
194
  ): Program {
175
195
  const managedNames = new Set(managedTools.map((tool) => tool.name));
176
196
  const indices: number[] = [];
177
- for (const definition of toolDefinitions(request)) {
197
+ for (const definition of findToolDefinitions(request)) {
178
198
  if (!definition.name || !managedNames.has(definition.name)) continue;
179
199
  for (let index = definition.start; index <= definition.end; index += 1) {
180
200
  indices.push(index);
181
201
  }
182
202
  }
183
- return clearIndices(request, indices);
203
+ return removeInstructions(request, indices);
184
204
  }
185
205
 
186
206
  function addProgramTool(request: Program, tool: ProgramTool): Program {
187
- return addTool(
207
+ return appendTool(
188
208
  request,
189
209
  tool.name,
190
210
  tool.description,
package/src/observe.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  import {
2
2
  cloneProgram,
3
- deltaText,
4
- finishReason,
5
- hasToolDelta,
3
+ delta,
4
+ getFinishReason,
6
5
  type Program,
7
6
  } from "@neutrome/lil-engine";
8
7
 
8
+ /**
9
+ * Callbacks fired while {@link observeExecutionStream} consumes a stream.
10
+ */
9
11
  export type StreamObservationHooks = {
10
12
  onTextStart?: () => void;
11
13
  onTextChunk?: (text: string, chunk: Program) => void;
12
14
  };
13
15
 
16
+ /**
17
+ * What a stream turned out to be: plain text, or a tool request.
18
+ */
14
19
  export type ObservedExecution =
15
20
  | {
16
21
  mode: "text";
@@ -24,6 +29,16 @@ export type ObservedExecution =
24
29
  chunks: Program[];
25
30
  };
26
31
 
32
+ /**
33
+ * Consumes a stream and reports whether it answered with text or tool calls.
34
+ *
35
+ * Chunks are buffered until the answer's shape is known, so a caller can decide
36
+ * what to forward. Tool-mode results carry every chunk for replay.
37
+ *
38
+ * @param source - Chunks to consume.
39
+ * @param hooks - Callbacks fired as text arrives.
40
+ * @returns What the stream turned out to be.
41
+ */
27
42
  export async function observeExecutionStream(
28
43
  source: AsyncIterable<Program>,
29
44
  hooks: StreamObservationHooks = {},
@@ -35,9 +50,9 @@ export async function observeExecutionStream(
35
50
  let emittedText = false;
36
51
 
37
52
  for await (const chunk of source) {
38
- const text = deltaText(chunk);
53
+ const text = delta.extractContentText(chunk);
39
54
  const toolRequested =
40
- hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
55
+ delta.hasToolCall(chunk) || getFinishReason(chunk) === "tool_calls";
41
56
 
42
57
  if (mode === "pending") {
43
58
  const clone = cloneProgram(chunk);
@@ -57,7 +72,7 @@ export async function observeExecutionStream(
57
72
  emittedText = true;
58
73
  hooks.onTextStart?.();
59
74
  for (const pendingChunk of pending.splice(0)) {
60
- const pendingText = deltaText(pendingChunk);
75
+ const pendingText = delta.extractContentText(pendingChunk);
61
76
  if (pendingText) {
62
77
  transcript += pendingText;
63
78
  hooks.onTextChunk?.(pendingText, pendingChunk);