@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.
- package/README.md +24 -21
- package/etc/api/index.api.md +96 -0
- package/etc/api/loops.api.md +108 -0
- package/etc/api/managed.api.md +102 -0
- package/etc/api/primitives.api.md +39 -0
- package/etc/api/stream.api.md +46 -0
- package/etc/api/tools.api.md +103 -0
- package/package.json +8 -9
- package/src/index.ts +41 -15
- package/src/loops/goal.ts +111 -0
- package/src/loops/index.ts +41 -110
- package/src/loops/shared.ts +27 -0
- package/src/managed/attachment-to-text.ts +35 -11
- package/src/managed/capabilities.ts +65 -25
- package/src/managed/index.ts +0 -14
- package/src/observe.ts +21 -6
- package/src/output.ts +13 -25
- package/src/primitives/index.ts +69 -0
- package/src/stream/index.ts +2 -6
- package/src/stream/stages.ts +15 -11
- package/src/tools-support.ts +61 -30
- package/src/tools.ts +37 -20
- package/src/types.ts +129 -4
- package/test/lilsdk-ts.test.ts +71 -341
- package/test/tools.test.ts +69 -21
- package/src/managed/two-pass-request.ts +0 -164
- package/src/managed/two-pass.ts +0 -171
- package/src/synthetic/index.ts +0 -134
|
@@ -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
|
+
}
|
package/src/loops/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
2
|
+
appendToolInteraction,
|
|
3
|
+
extractAttachments,
|
|
4
|
+
extractContentText,
|
|
5
|
+
findMessages,
|
|
3
6
|
insertAfter,
|
|
4
7
|
isAttachmentOpcode,
|
|
5
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
appendTool,
|
|
3
|
+
findToolDefinitions,
|
|
4
|
+
ProgramView,
|
|
5
|
+
removeInstructions,
|
|
6
6
|
type Program,
|
|
7
7
|
type ProgramTool,
|
|
8
8
|
} from "@neutrome/lil-engine";
|
|
9
|
-
import {
|
|
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 =
|
|
55
|
+
const tools = new ProgramView(request).tools;
|
|
37
56
|
if (tools.length === 0) return ctx.invoke(inner, request);
|
|
38
57
|
|
|
39
|
-
const
|
|
40
|
-
|
|
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 =
|
|
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
|
|
55
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
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
|
|
203
|
+
return removeInstructions(request, indices);
|
|
164
204
|
}
|
|
165
205
|
|
|
166
206
|
function addProgramTool(request: Program, tool: ProgramTool): Program {
|
|
167
|
-
return
|
|
207
|
+
return appendTool(
|
|
168
208
|
request,
|
|
169
209
|
tool.name,
|
|
170
210
|
tool.description,
|
package/src/managed/index.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
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 =
|
|
53
|
+
const text = delta.extractContentText(chunk);
|
|
39
54
|
const toolRequested =
|
|
40
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
}
|