@neutrome/lilsdk 0.4.5 → 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.
@@ -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,46 +17,82 @@ 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;
26
+ skipPrefixes?: readonly string[];
23
27
  };
24
28
 
25
29
  type CapabilitySelection = { toolName: string; remaining: number };
26
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
+ */
27
45
  export function createCapabilitiesExecutor(
28
46
  inner: ExecutorInput,
29
47
  options: CapabilitiesExecutorOptions = {},
30
48
  ): Executor {
31
49
  const enabledIterations = positiveInteger(options.enabledIterations ?? 5);
32
50
  const cacheKey = options.cacheKey ?? ((ctx) => ctx.requestId);
51
+ const skipPrefixes = options.skipPrefixes ?? [];
33
52
 
34
53
  return {
35
54
  async execute(request, ctx) {
36
- const tools = viewProgram(request).tools;
55
+ const tools = new ProgramView(request).tools;
37
56
  if (tools.length === 0) return ctx.invoke(inner, request);
38
57
 
39
- const key = selectionKey(cacheKey(ctx));
40
- await discardMissingSelection(ctx, key, tools);
41
- return connectCapabilities(inner, tools, key, enabledIterations).execute(
42
- withoutTools(request),
43
- ctx,
58
+ const managedTools = tools.filter(
59
+ (tool) => !skipPrefixes.some((prefix) => tool.name.startsWith(prefix)),
44
60
  );
61
+ if (managedTools.length === 0) return ctx.invoke(inner, request);
62
+
63
+ const key = selectionKey(cacheKey(ctx));
64
+ await discardMissingSelection(ctx, key, managedTools);
65
+ return connectCapabilities(
66
+ inner,
67
+ managedTools,
68
+ key,
69
+ enabledIterations,
70
+ ).execute(withoutManagedTools(request, managedTools), ctx);
45
71
  },
46
72
 
47
73
  async *stream(request, ctx) {
48
- const tools = viewProgram(request).tools;
74
+ const tools = new ProgramView(request).tools;
49
75
  if (tools.length === 0) {
50
76
  yield* ctx.invokeStream(inner, request);
51
77
  return;
52
78
  }
53
79
 
54
- const key = selectionKey(cacheKey(ctx));
55
- await discardMissingSelection(ctx, key, tools);
56
- yield* connectCapabilities(inner, tools, key, enabledIterations).stream(
57
- withoutTools(request),
58
- ctx,
80
+ const managedTools = tools.filter(
81
+ (tool) => !skipPrefixes.some((prefix) => tool.name.startsWith(prefix)),
59
82
  );
83
+ if (managedTools.length === 0) {
84
+ yield* ctx.invokeStream(inner, request);
85
+ return;
86
+ }
87
+
88
+ const key = selectionKey(cacheKey(ctx));
89
+ await discardMissingSelection(ctx, key, managedTools);
90
+ yield* connectCapabilities(
91
+ inner,
92
+ managedTools,
93
+ key,
94
+ enabledIterations,
95
+ ).stream(withoutManagedTools(request, managedTools), ctx);
60
96
  },
61
97
  };
62
98
  }
@@ -82,10 +118,9 @@ function connectCapabilities(
82
118
  },
83
119
  };
84
120
 
85
- return connectTools(
86
- [learnCapabilityTool(tools, key, enabledIterations)],
87
- selectedInner,
88
- );
121
+ return createToolsExecutor(selectedInner, [
122
+ learnCapabilityTool(tools, key, enabledIterations),
123
+ ]);
89
124
  }
90
125
 
91
126
  function learnCapabilityTool(
@@ -153,18 +188,23 @@ async function discardMissingSelection(
153
188
  }
154
189
  }
155
190
 
156
- function withoutTools(request: Program): Program {
191
+ function withoutManagedTools(
192
+ request: Program,
193
+ managedTools: readonly ProgramTool[],
194
+ ): Program {
195
+ const managedNames = new Set(managedTools.map((tool) => tool.name));
157
196
  const indices: number[] = [];
158
- for (const definition of toolDefinitions(request)) {
197
+ for (const definition of findToolDefinitions(request)) {
198
+ if (!definition.name || !managedNames.has(definition.name)) continue;
159
199
  for (let index = definition.start; index <= definition.end; index += 1) {
160
200
  indices.push(index);
161
201
  }
162
202
  }
163
- return clearIndices(request, indices);
203
+ return removeInstructions(request, indices);
164
204
  }
165
205
 
166
206
  function addProgramTool(request: Program, tool: ProgramTool): Program {
167
- return addTool(
207
+ return appendTool(
168
208
  request,
169
209
  tool.name,
170
210
  tool.description,
@@ -5,17 +5,3 @@ export type {
5
5
  AttachmentReaderRule,
6
6
  AttachmentToTextOptions,
7
7
  } from "./attachment-to-text.ts";
8
-
9
- export {
10
- appendInternalDraft,
11
- createTwoPassExecutor,
12
- INTERNAL_DRAFT_CALL_ID,
13
- INTERNAL_DRAFT_TOOL_NAME,
14
- } from "./two-pass.ts";
15
-
16
- export type {
17
- InternalDraft,
18
- TwoPassExecutorOptions,
19
- TwoPassSettings,
20
- TwoPassSettingsResolver,
21
- } from "./two-pass.ts";
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);
package/src/output.ts CHANGED
@@ -1,31 +1,19 @@
1
- import {
2
- createProgram,
3
- Opcode,
4
- streamEnd,
5
- streamStart,
6
- streamTextDelta,
7
- type Program,
8
- } from "@neutrome/lil-engine";
1
+ import { delta } from "@neutrome/lil-engine";
9
2
  import type { OutputSink } from "./types.ts";
10
3
 
11
- export async function writeReasoning(
4
+ /**
5
+ * Writes a model-reasoning chunk to a sink.
6
+ *
7
+ * @param sink - Sink to write to.
8
+ * @param text - Reasoning text to emit.
9
+ * @example
10
+ * ```ts
11
+ * await writeThinking(sink, "Checking the attachment first.");
12
+ * ```
13
+ */
14
+ export async function writeThinking(
12
15
  sink: OutputSink,
13
16
  text: string,
14
17
  ): Promise<void> {
15
- await sink.write(streamReasoningDelta(text));
16
- }
17
-
18
- export function streamTextResponse(text: string): Program[] {
19
- return [streamStart(), streamTextDelta(text), streamEnd()];
20
- }
21
-
22
- export function streamReasoningDelta(text: string): Program {
23
- return createProgram({
24
- code: [
25
- {
26
- opcode: Opcode.STREAM_THINK_DELTA,
27
- value: { kind: "string", value: text },
28
- },
29
- ],
30
- });
18
+ await sink.write(delta.thinking(text));
31
19
  }