@neutrome/lilsdk 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.ts +3 -2
- package/src/loops/index.ts +24 -18
- package/src/managed/attachment-to-text.ts +175 -0
- package/src/managed/capabilities.ts +215 -0
- package/src/managed/index.ts +9 -6
- package/src/managed/{twoPassExecutor.ts → two-pass.ts} +19 -48
- package/src/stream/index.ts +1 -0
- package/src/stream/stages.ts +25 -0
- package/src/tools.ts +10 -5
- package/src/types.ts +18 -30
- package/test/lilsdk-ts.test.ts +203 -32
- package/test/tools.test.ts +190 -194
- package/src/managed/target-executor.ts +0 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/lilsdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./managed": "./src/managed/index.ts"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@neutrome/lil-engine": "0.
|
|
16
|
+
"@neutrome/lil-engine": "0.4.1"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^25.9.3",
|
package/src/index.ts
CHANGED
package/src/loops/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProgram, deltaText, type Program } from "@neutrome/lil-engine";
|
|
2
|
-
import type { Executor, ExecutorContext } from "../types.ts";
|
|
2
|
+
import type { Executor, ExecutorContext, ExecutorInput } from "../types.ts";
|
|
3
|
+
import { completeStream, streamStage } from "../stream/stages.ts";
|
|
3
4
|
import { appendAssistantMessage } from "../synthetic/index.ts";
|
|
4
5
|
|
|
5
6
|
export type RetryOptions = {
|
|
@@ -17,8 +18,8 @@ export type FallbackOptions = {
|
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export type GoalExecutorOptions = {
|
|
20
|
-
draft:
|
|
21
|
-
review:
|
|
21
|
+
draft: ExecutorInput;
|
|
22
|
+
review: ExecutorInput;
|
|
22
23
|
refine: (
|
|
23
24
|
request: Program,
|
|
24
25
|
answer: Program,
|
|
@@ -30,7 +31,7 @@ export type GoalExecutorOptions = {
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
export function retry(
|
|
33
|
-
executor:
|
|
34
|
+
executor: ExecutorInput,
|
|
34
35
|
options: RetryOptions = {},
|
|
35
36
|
): Executor {
|
|
36
37
|
const attempts = positiveInteger(options.attempts ?? 2, "retry attempts");
|
|
@@ -38,7 +39,7 @@ export function retry(
|
|
|
38
39
|
return {
|
|
39
40
|
execute(request, ctx) {
|
|
40
41
|
return runRetry(
|
|
41
|
-
(attempt) =>
|
|
42
|
+
(attempt) => childContext(ctx, attempt).invoke(executor, request),
|
|
42
43
|
attempts,
|
|
43
44
|
options,
|
|
44
45
|
);
|
|
@@ -50,9 +51,9 @@ export function retry(
|
|
|
50
51
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
51
52
|
let emitted = false;
|
|
52
53
|
try {
|
|
53
|
-
for await (const chunk of
|
|
54
|
+
for await (const chunk of childContext(ctx, attempt).invokeStream(
|
|
55
|
+
executor,
|
|
54
56
|
request,
|
|
55
|
-
childContext(ctx, attempt),
|
|
56
57
|
)) {
|
|
57
58
|
emitted = true;
|
|
58
59
|
yield chunk;
|
|
@@ -77,7 +78,7 @@ export function retry(
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export function fallback(
|
|
80
|
-
executors: readonly
|
|
81
|
+
executors: readonly ExecutorInput[],
|
|
81
82
|
options: FallbackOptions = {},
|
|
82
83
|
): Executor {
|
|
83
84
|
if (executors.length === 0) {
|
|
@@ -88,7 +89,7 @@ export function fallback(
|
|
|
88
89
|
execute(request, ctx) {
|
|
89
90
|
return runFallback(
|
|
90
91
|
(executor, index) =>
|
|
91
|
-
|
|
92
|
+
childContext(ctx, index + 1).invoke(executor, request),
|
|
92
93
|
executors,
|
|
93
94
|
options,
|
|
94
95
|
);
|
|
@@ -100,9 +101,9 @@ export function fallback(
|
|
|
100
101
|
for (const [index, executor] of executors.entries()) {
|
|
101
102
|
let emitted = false;
|
|
102
103
|
try {
|
|
103
|
-
for await (const chunk of
|
|
104
|
+
for await (const chunk of childContext(ctx, index + 1).invokeStream(
|
|
105
|
+
executor,
|
|
104
106
|
request,
|
|
105
|
-
childContext(ctx, index + 1),
|
|
106
107
|
)) {
|
|
107
108
|
emitted = true;
|
|
108
109
|
yield chunk;
|
|
@@ -139,8 +140,8 @@ export function createGoalExecutor(options: GoalExecutorOptions): Executor {
|
|
|
139
140
|
|
|
140
141
|
for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
|
|
141
142
|
const attemptContext = childContext(ctx, attempt);
|
|
142
|
-
answer = await options.draft
|
|
143
|
-
const review = await options.review
|
|
143
|
+
answer = await attemptContext.invoke(options.draft, current);
|
|
144
|
+
const review = await attemptContext.invoke(options.review, answer);
|
|
144
145
|
if (await options.satisfied(review, attempt)) return answer;
|
|
145
146
|
if (attempt < maxIterations) {
|
|
146
147
|
current = await options.refine(current, answer, review, attempt);
|
|
@@ -157,19 +158,24 @@ export function createGoalExecutor(options: GoalExecutorOptions): Executor {
|
|
|
157
158
|
for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
|
|
158
159
|
const attemptContext = childContext(ctx, attempt);
|
|
159
160
|
answer = yield* streamGoalStage(
|
|
160
|
-
options.draft
|
|
161
|
+
streamStage(attemptContext.invokeStream(options.draft, current)),
|
|
161
162
|
current,
|
|
162
163
|
);
|
|
163
164
|
const review = yield* streamGoalStage(
|
|
164
|
-
options.review
|
|
165
|
+
streamStage(attemptContext.invokeStream(options.review, answer)),
|
|
165
166
|
answer,
|
|
166
167
|
);
|
|
167
168
|
|
|
168
|
-
if (await options.satisfied(review, attempt))
|
|
169
|
+
if (await options.satisfied(review, attempt)) {
|
|
170
|
+
yield completeStream();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
169
173
|
if (attempt < maxIterations) {
|
|
170
174
|
current = await options.refine(current, answer, review, attempt);
|
|
171
175
|
}
|
|
172
176
|
}
|
|
177
|
+
|
|
178
|
+
yield completeStream();
|
|
173
179
|
},
|
|
174
180
|
};
|
|
175
181
|
}
|
|
@@ -218,8 +224,8 @@ async function runRetry<T>(
|
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
async function runFallback<T>(
|
|
221
|
-
operation: (executor:
|
|
222
|
-
executors: readonly
|
|
227
|
+
operation: (executor: ExecutorInput, index: number) => Promise<T>,
|
|
228
|
+
executors: readonly ExecutorInput[],
|
|
223
229
|
options: FallbackOptions,
|
|
224
230
|
): Promise<T> {
|
|
225
231
|
let lastError: unknown;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
contentText,
|
|
3
|
+
insertAfter,
|
|
4
|
+
isAttachmentOpcode,
|
|
5
|
+
messages,
|
|
6
|
+
programAttachments,
|
|
7
|
+
type Program,
|
|
8
|
+
type ProgramAttachment,
|
|
9
|
+
} from "@neutrome/lil-engine";
|
|
10
|
+
import {
|
|
11
|
+
appendToolInteraction,
|
|
12
|
+
prependSystemPrompt,
|
|
13
|
+
} from "../synthetic/index.ts";
|
|
14
|
+
import type { Executor, ExecutorContext, ExecutorInput } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
const toolName = "attachment_to_text";
|
|
17
|
+
const defaultPrompt =
|
|
18
|
+
"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
|
+
|
|
20
|
+
export type AttachmentReaderRule = {
|
|
21
|
+
mimeTypes: readonly string[];
|
|
22
|
+
executor: ExecutorInput;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type AttachmentToTextOptions = {
|
|
26
|
+
systemPrompt?: string;
|
|
27
|
+
cacheKey?: (attachment: ProgramAttachment, ctx: ExecutorContext) => string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createAttachmentToTextExecutor(
|
|
31
|
+
inner: ExecutorInput,
|
|
32
|
+
rules: readonly AttachmentReaderRule[],
|
|
33
|
+
options: AttachmentToTextOptions = {},
|
|
34
|
+
): Executor {
|
|
35
|
+
if (rules.length === 0)
|
|
36
|
+
throw new Error("attachment-to-text requires at least one reader rule");
|
|
37
|
+
|
|
38
|
+
const prompt = options.systemPrompt ?? defaultPrompt;
|
|
39
|
+
return {
|
|
40
|
+
async execute(request, ctx) {
|
|
41
|
+
return ctx.invoke(
|
|
42
|
+
inner,
|
|
43
|
+
await prepareRequest(request, ctx, rules, prompt, options),
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
async *stream(request, ctx) {
|
|
47
|
+
yield* ctx.invokeStream(
|
|
48
|
+
inner,
|
|
49
|
+
await prepareRequest(request, ctx, rules, prompt, options),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function prepareRequest(
|
|
56
|
+
request: Program,
|
|
57
|
+
ctx: ExecutorContext,
|
|
58
|
+
rules: readonly AttachmentReaderRule[],
|
|
59
|
+
prompt: string,
|
|
60
|
+
options: AttachmentToTextOptions,
|
|
61
|
+
): Promise<Program> {
|
|
62
|
+
const attachments = programAttachments(request);
|
|
63
|
+
if (attachments.length === 0) return request;
|
|
64
|
+
const latestUserStart = [...attachments]
|
|
65
|
+
.filter((attachment) => attachment.message.role === "user")
|
|
66
|
+
.at(-1)?.message.start;
|
|
67
|
+
let result = stripAttachments(request);
|
|
68
|
+
const sourceMessages = messages(request);
|
|
69
|
+
|
|
70
|
+
for (const attachment of [...attachments].reverse()) {
|
|
71
|
+
const rule = rules.find((candidate) =>
|
|
72
|
+
candidate.mimeTypes.some((mime) =>
|
|
73
|
+
matchesMime(mime, attachment.mimeType),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
if (!rule) {
|
|
77
|
+
if (attachment.message.start === latestUserStart) {
|
|
78
|
+
throw new Error(`No attachment reader matches ${attachment.mimeType}`);
|
|
79
|
+
}
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Missing cached attachment description for historical ${attachment.mimeType}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const key = await attachmentCacheKey(attachment, ctx, options);
|
|
85
|
+
let description = await ctx.cache.get(key);
|
|
86
|
+
if (!description) {
|
|
87
|
+
if (attachment.message.start !== latestUserStart) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Missing cached attachment description for historical ${attachment.mimeType}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const readerRequest = readerProgram(request, attachment, prompt);
|
|
93
|
+
description = contentText(await ctx.invoke(rule.executor, readerRequest));
|
|
94
|
+
if (!description.trim())
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Attachment reader returned no text for ${attachment.mimeType}`,
|
|
97
|
+
);
|
|
98
|
+
await ctx.cache.put(key, description);
|
|
99
|
+
}
|
|
100
|
+
const interaction = {
|
|
101
|
+
callId: `attachment_${key.slice(-24)}`,
|
|
102
|
+
name: toolName,
|
|
103
|
+
args: {
|
|
104
|
+
mimeType: attachment.mimeType,
|
|
105
|
+
...(attachment.name ? { name: attachment.name } : {}),
|
|
106
|
+
context:
|
|
107
|
+
"The user attached this file. Its binary media was replaced with the reconstructed text result below.",
|
|
108
|
+
},
|
|
109
|
+
result: `Reconstructed content for the file the user attached. The original binary media is not present in this context.\n\n${description}`,
|
|
110
|
+
};
|
|
111
|
+
const messageIndex = sourceMessages.findIndex(
|
|
112
|
+
(message) => message.start === attachment.message.start,
|
|
113
|
+
);
|
|
114
|
+
const targetMessage = messages(result)[messageIndex];
|
|
115
|
+
if (!targetMessage)
|
|
116
|
+
throw new Error("Attachment message was removed from context");
|
|
117
|
+
const synthetic = appendToolInteraction(
|
|
118
|
+
{ code: [], buffers: [] },
|
|
119
|
+
interaction,
|
|
120
|
+
);
|
|
121
|
+
result = insertAfter(result, targetMessage.end, synthetic.code);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readerProgram(
|
|
127
|
+
request: Program,
|
|
128
|
+
attachment: ProgramAttachment,
|
|
129
|
+
prompt: string,
|
|
130
|
+
): Program {
|
|
131
|
+
return prependSystemPrompt(
|
|
132
|
+
{
|
|
133
|
+
code: request.code.slice(
|
|
134
|
+
attachment.message.start,
|
|
135
|
+
attachment.message.end + 1,
|
|
136
|
+
),
|
|
137
|
+
buffers: request.buffers.map((buffer) => buffer.slice()),
|
|
138
|
+
},
|
|
139
|
+
prompt,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function stripAttachments(request: Program): Program {
|
|
144
|
+
return {
|
|
145
|
+
code: request.code.filter(
|
|
146
|
+
(instruction) => !isAttachmentOpcode(instruction.opcode),
|
|
147
|
+
),
|
|
148
|
+
buffers: request.buffers.map((buffer) => buffer.slice()),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function attachmentCacheKey(
|
|
153
|
+
attachment: ProgramAttachment,
|
|
154
|
+
ctx: ExecutorContext,
|
|
155
|
+
options: AttachmentToTextOptions,
|
|
156
|
+
): Promise<string> {
|
|
157
|
+
if (options.cacheKey)
|
|
158
|
+
return `attachment-to-text:${options.cacheKey(attachment, ctx)}`;
|
|
159
|
+
const bytes = new TextEncoder().encode(
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
mimeType: attachment.mimeType,
|
|
162
|
+
url: attachment.url,
|
|
163
|
+
name: attachment.name,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
167
|
+
return `attachment-to-text:${Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function matchesMime(pattern: string, mimeType: string): boolean {
|
|
171
|
+
return (
|
|
172
|
+
pattern === mimeType ||
|
|
173
|
+
(pattern.endsWith("/*") && mimeType.startsWith(pattern.slice(0, -1)))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addTool,
|
|
3
|
+
clearIndices,
|
|
4
|
+
toolDefinitions,
|
|
5
|
+
viewProgram,
|
|
6
|
+
type Program,
|
|
7
|
+
type ProgramTool,
|
|
8
|
+
} from "@neutrome/lil-engine";
|
|
9
|
+
import { connectTools } from "../tools.ts";
|
|
10
|
+
import type {
|
|
11
|
+
Executor,
|
|
12
|
+
ExecutorContext,
|
|
13
|
+
ExecutorInput,
|
|
14
|
+
Tool,
|
|
15
|
+
} from "../types.ts";
|
|
16
|
+
|
|
17
|
+
const capabilityToolName = "learn_capability";
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
|
|
20
|
+
export type CapabilitiesExecutorOptions = {
|
|
21
|
+
enabledIterations?: number;
|
|
22
|
+
cacheKey?: (ctx: ExecutorContext) => string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type CapabilitySelection = { toolName: string; remaining: number };
|
|
26
|
+
|
|
27
|
+
export function createCapabilitiesExecutor(
|
|
28
|
+
inner: ExecutorInput,
|
|
29
|
+
options: CapabilitiesExecutorOptions = {},
|
|
30
|
+
): Executor {
|
|
31
|
+
const enabledIterations = positiveInteger(options.enabledIterations ?? 5);
|
|
32
|
+
const cacheKey = options.cacheKey ?? ((ctx) => ctx.requestId);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async execute(request, ctx) {
|
|
36
|
+
const tools = viewProgram(request).tools;
|
|
37
|
+
if (tools.length === 0) return ctx.invoke(inner, request);
|
|
38
|
+
|
|
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,
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async *stream(request, ctx) {
|
|
48
|
+
const tools = viewProgram(request).tools;
|
|
49
|
+
if (tools.length === 0) {
|
|
50
|
+
yield* ctx.invokeStream(inner, request);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
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,
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function connectCapabilities(
|
|
65
|
+
inner: ExecutorInput,
|
|
66
|
+
tools: readonly ProgramTool[],
|
|
67
|
+
key: string,
|
|
68
|
+
enabledIterations: number,
|
|
69
|
+
): Executor {
|
|
70
|
+
const selectedInner: Executor = {
|
|
71
|
+
async execute(request, ctx) {
|
|
72
|
+
return ctx.invoke(
|
|
73
|
+
inner,
|
|
74
|
+
await withSelectedTool(request, ctx, key, tools),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
async *stream(request, ctx) {
|
|
78
|
+
yield* ctx.invokeStream(
|
|
79
|
+
inner,
|
|
80
|
+
await withSelectedTool(request, ctx, key, tools),
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return connectTools(
|
|
86
|
+
[learnCapabilityTool(tools, key, enabledIterations)],
|
|
87
|
+
selectedInner,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function learnCapabilityTool(
|
|
92
|
+
tools: readonly ProgramTool[],
|
|
93
|
+
key: string,
|
|
94
|
+
enabledIterations: number,
|
|
95
|
+
): Tool {
|
|
96
|
+
return {
|
|
97
|
+
name: capabilityToolName,
|
|
98
|
+
description: `Enable one capability for the next ${enabledIterations} upstream iterations. Available capabilities: ${tools.map((tool) => tool.name).join(", ")}.`,
|
|
99
|
+
schema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: { capability: { type: "string" } },
|
|
102
|
+
required: ["capability"],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
},
|
|
105
|
+
async execute(args, ctx) {
|
|
106
|
+
const name = typeof args.capability === "string" ? args.capability : "";
|
|
107
|
+
if (!tools.some((tool) => tool.name === name)) {
|
|
108
|
+
return `Unknown capability: ${name || "(missing)"}.`;
|
|
109
|
+
}
|
|
110
|
+
await writeSelection(ctx, key, {
|
|
111
|
+
toolName: name,
|
|
112
|
+
remaining: enabledIterations,
|
|
113
|
+
});
|
|
114
|
+
return `Enabled ${name} for the next ${enabledIterations} upstream iterations.`;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function withSelectedTool(
|
|
120
|
+
request: Program,
|
|
121
|
+
ctx: ExecutorContext,
|
|
122
|
+
key: string,
|
|
123
|
+
tools: readonly ProgramTool[],
|
|
124
|
+
): Promise<Program> {
|
|
125
|
+
const selection = await readSelection(ctx, key);
|
|
126
|
+
if (!selection || selection.remaining < 1) return request;
|
|
127
|
+
|
|
128
|
+
const tool = tools.find((candidate) => candidate.name === selection.toolName);
|
|
129
|
+
if (!tool) {
|
|
130
|
+
await ctx.cache.delete(key);
|
|
131
|
+
return request;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (selection.remaining === 1) {
|
|
135
|
+
await ctx.cache.delete(key);
|
|
136
|
+
} else {
|
|
137
|
+
await writeSelection(ctx, key, {
|
|
138
|
+
...selection,
|
|
139
|
+
remaining: selection.remaining - 1,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return addProgramTool(request, tool);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function discardMissingSelection(
|
|
146
|
+
ctx: ExecutorContext,
|
|
147
|
+
key: string,
|
|
148
|
+
tools: readonly ProgramTool[],
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const selection = await readSelection(ctx, key);
|
|
151
|
+
if (selection && !tools.some((tool) => tool.name === selection.toolName)) {
|
|
152
|
+
await ctx.cache.delete(key);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function withoutTools(request: Program): Program {
|
|
157
|
+
const indices: number[] = [];
|
|
158
|
+
for (const definition of toolDefinitions(request)) {
|
|
159
|
+
for (let index = definition.start; index <= definition.end; index += 1) {
|
|
160
|
+
indices.push(index);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return clearIndices(request, indices);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function addProgramTool(request: Program, tool: ProgramTool): Program {
|
|
167
|
+
return addTool(
|
|
168
|
+
request,
|
|
169
|
+
tool.name,
|
|
170
|
+
tool.description,
|
|
171
|
+
encoder.encode(JSON.stringify(tool.schema)),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function readSelection(
|
|
176
|
+
ctx: ExecutorContext,
|
|
177
|
+
key: string,
|
|
178
|
+
): Promise<CapabilitySelection | null> {
|
|
179
|
+
const value = await ctx.cache.get(key);
|
|
180
|
+
if (!value) return null;
|
|
181
|
+
try {
|
|
182
|
+
const parsed: unknown = JSON.parse(value);
|
|
183
|
+
if (
|
|
184
|
+
parsed &&
|
|
185
|
+
typeof parsed === "object" &&
|
|
186
|
+
typeof (parsed as CapabilitySelection).toolName === "string" &&
|
|
187
|
+
Number.isInteger((parsed as CapabilitySelection).remaining)
|
|
188
|
+
) {
|
|
189
|
+
return parsed as CapabilitySelection;
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// Treat malformed cache data as an expired selection.
|
|
193
|
+
}
|
|
194
|
+
await ctx.cache.delete(key);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeSelection(
|
|
199
|
+
ctx: ExecutorContext,
|
|
200
|
+
key: string,
|
|
201
|
+
selection: CapabilitySelection,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
return ctx.cache.put(key, JSON.stringify(selection));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function selectionKey(key: string): string {
|
|
207
|
+
return `capabilities:${key}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function positiveInteger(value: number): number {
|
|
211
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
212
|
+
throw new Error("enabledIterations must be a positive integer");
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
package/src/managed/index.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createCapabilitiesExecutor } from "./capabilities.ts";
|
|
2
|
+
export type { CapabilitiesExecutorOptions } from "./capabilities.ts";
|
|
3
|
+
export { createAttachmentToTextExecutor } from "./attachment-to-text.ts";
|
|
4
|
+
export type {
|
|
5
|
+
AttachmentReaderRule,
|
|
6
|
+
AttachmentToTextOptions,
|
|
7
|
+
} from "./attachment-to-text.ts";
|
|
2
8
|
|
|
3
9
|
export {
|
|
4
10
|
appendInternalDraft,
|
|
5
11
|
createTwoPassExecutor,
|
|
6
12
|
INTERNAL_DRAFT_CALL_ID,
|
|
7
13
|
INTERNAL_DRAFT_TOOL_NAME,
|
|
8
|
-
|
|
9
|
-
streamExecutor,
|
|
10
|
-
} from "./twoPassExecutor.ts";
|
|
14
|
+
} from "./two-pass.ts";
|
|
11
15
|
|
|
12
16
|
export type {
|
|
13
17
|
InternalDraft,
|
|
14
|
-
ModelExecutor,
|
|
15
18
|
TwoPassExecutorOptions,
|
|
16
19
|
TwoPassSettings,
|
|
17
20
|
TwoPassSettingsResolver,
|
|
18
|
-
} from "./
|
|
21
|
+
} from "./two-pass.ts";
|