@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/README.md +24 -17
- package/etc/api/index.api.md +96 -103
- package/etc/api/loops.api.md +108 -108
- package/etc/api/managed.api.md +102 -102
- package/etc/api/primitives.api.md +39 -39
- package/etc/api/stream.api.md +46 -55
- package/etc/api/tools.api.md +103 -103
- package/package.json +8 -10
- 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 +42 -22
- 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 +9 -3
- package/src/tools.ts +36 -19
- package/src/types.ts +129 -4
- package/test/lilsdk-ts.test.ts +47 -45
- package/test/tools.test.ts +22 -20
- package/etc/api/synthetic.api.md +0 -46
- package/etc/api/types.api.md +0 -103
- package/src/synthetic/index.ts +0 -134
package/src/index.ts
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
}
|
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,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 =
|
|
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(
|
|
49
|
-
|
|
50
|
-
|
|
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 =
|
|
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(
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
203
|
+
return removeInstructions(request, indices);
|
|
184
204
|
}
|
|
185
205
|
|
|
186
206
|
function addProgramTool(request: Program, tool: ProgramTool): Program {
|
|
187
|
-
return
|
|
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
|
-
|
|
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);
|