@neutrome/lilsdk-ts 0.1.2 → 0.2.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 +19 -11
- package/package.json +15 -8
- package/src/index.ts +14 -9
- package/src/loops/index.ts +190 -0
- package/src/managed/index.ts +15 -0
- package/src/managed/twoPassExecutor.ts +214 -0
- package/src/managed/wrap.ts +9 -0
- package/src/observe.ts +94 -2
- package/src/output.ts +18 -3
- package/src/primitives/index.ts +120 -0
- package/src/stream/index.ts +4 -0
- package/src/synthetic/index.ts +151 -0
- package/src/tools.ts +357 -0
- package/src/types.ts +82 -20
- package/test/lilsdk-ts.test.ts +244 -84
- package/test/tools.test.ts +259 -0
- package/tsconfig.json +13 -1
- package/src/execution.ts +0 -64
- package/src/request.ts +0 -21
package/README.md
CHANGED
|
@@ -3,18 +3,26 @@
|
|
|
3
3
|
High-level TypeScript SDK for transport-neutral model authoring on top of `@neutrome/lil-engine`.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
writeReasoning,
|
|
11
|
-
type ModelHandler,
|
|
12
|
-
} from "@neutrome/lilsdk-ts";
|
|
6
|
+
import { createTwoPassExecutor, type Executor } from "@neutrome/lilsdk-ts";
|
|
7
|
+
import { retry } from "@neutrome/lilsdk-ts/loops";
|
|
8
|
+
import { observeExecutionStream, writeReasoning } from "@neutrome/lilsdk-ts/stream";
|
|
9
|
+
import { connectTools } from "@neutrome/lilsdk-ts/tools";
|
|
13
10
|
```
|
|
14
11
|
|
|
15
12
|
This package owns:
|
|
16
13
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
14
|
+
- executor and tool contracts
|
|
15
|
+
- `@neutrome/lilsdk-ts/primitives`: structural program helpers such as `walk`, `map`, `reduce`, `has`, `find`, `indexOf`, `all`, and `any`
|
|
16
|
+
- `@neutrome/lilsdk-ts/synthetic`: request/message/tool synthesis helpers such as `makeRequest`, `makeResponse`, `makeMessage`, `makeToolCall`, `makeToolResponse`, and `appendToolInteraction`
|
|
17
|
+
- `@neutrome/lilsdk-ts/tools`: `connectTools` and `withTools` tool-call loops
|
|
18
|
+
- `@neutrome/lilsdk-ts/stream`: stream observation and output helpers
|
|
19
|
+
- `@neutrome/lilsdk-ts/loops`: `retry`, `fallback`, and `goal` executor control-flow helpers
|
|
20
|
+
- `@neutrome/lilsdk-ts/managed/twoPassExecutor`: `createTwoPassExecutor` and model invocation helpers
|
|
21
|
+
- multi-pass authoring primitives used by virtual models
|
|
22
|
+
|
|
23
|
+
The root export is intentionally small: SDK contracts plus the most common
|
|
24
|
+
high-level two-pass executor helper.
|
|
25
|
+
|
|
26
|
+
It intentionally does not own HTTP routing, provider credentials, target
|
|
27
|
+
resolution, or provider JSON conversion. Those live in `@neutrome/open-ai-router`
|
|
28
|
+
and `@neutrome/lil-engine`.
|
package/package.json
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/lilsdk-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".": "./src/index.ts"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
6
|
+
".": "./src/index.ts",
|
|
7
|
+
"./types": "./src/types.ts",
|
|
8
|
+
"./primitives": "./src/primitives/index.ts",
|
|
9
|
+
"./synthetic": "./src/synthetic/index.ts",
|
|
10
|
+
"./stream": "./src/stream/index.ts",
|
|
11
|
+
"./tools": "./src/tools.ts",
|
|
12
|
+
"./loops": "./src/loops/index.ts",
|
|
13
|
+
"./managed": "./src/managed/index.ts"
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
13
|
-
"@neutrome/lil-engine": "0.
|
|
16
|
+
"@neutrome/lil-engine": "0.2.0"
|
|
14
17
|
},
|
|
15
18
|
"devDependencies": {
|
|
16
19
|
"@types/node": "^25.9.3",
|
|
17
20
|
"typescript": "^6.0.3",
|
|
18
21
|
"vitest": "^4.1.6"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
26
|
}
|
|
20
|
-
}
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
export type {
|
|
2
|
+
ExecutionTarget,
|
|
3
|
+
Executor,
|
|
4
|
+
ExecutorContext,
|
|
5
|
+
InvokeOptions,
|
|
6
|
+
OutputSink,
|
|
7
|
+
ProgramTransform,
|
|
8
|
+
SdkEvent,
|
|
9
|
+
SdkEventInput,
|
|
10
|
+
Tool,
|
|
11
|
+
TransformCapability,
|
|
12
|
+
TransformContext,
|
|
13
|
+
createSdkEvent,
|
|
14
|
+
} from "./types.ts";
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Program } from "@neutrome/lil-engine";
|
|
2
|
+
import type { Executor, ExecutorContext } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export type RetryOptions = {
|
|
5
|
+
attempts?: number;
|
|
6
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
|
|
7
|
+
onRetry?: (error: unknown, attempt: number) => void | Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type FallbackOptions = {
|
|
11
|
+
shouldFallback?: (error: unknown, executorIndex: number) => boolean | Promise<boolean>;
|
|
12
|
+
onFallback?: (error: unknown, executorIndex: number) => void | Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type GoalOptions = {
|
|
16
|
+
draft: Executor;
|
|
17
|
+
review: Executor;
|
|
18
|
+
refine: (
|
|
19
|
+
request: Program,
|
|
20
|
+
answer: Program,
|
|
21
|
+
review: Program,
|
|
22
|
+
attempt: number,
|
|
23
|
+
) => Program | Promise<Program>;
|
|
24
|
+
satisfied: (review: Program, attempt: number) => boolean | Promise<boolean>;
|
|
25
|
+
maxIterations?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function retry(executor: Executor, options: RetryOptions = {}): Executor {
|
|
29
|
+
const attempts = positiveInteger(options.attempts ?? 2, "retry attempts");
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
execute(request, ctx) {
|
|
33
|
+
return runRetry(
|
|
34
|
+
(attempt) => executor.execute(request, childContext(ctx, attempt)),
|
|
35
|
+
attempts,
|
|
36
|
+
options,
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
stream(request, ctx) {
|
|
41
|
+
return streamBuffered(() =>
|
|
42
|
+
runRetry(
|
|
43
|
+
(attempt) => collect(executor.stream(request, childContext(ctx, attempt))),
|
|
44
|
+
attempts,
|
|
45
|
+
options,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function fallback(executors: readonly Executor[], options: FallbackOptions = {}): Executor {
|
|
53
|
+
if (executors.length === 0) {
|
|
54
|
+
throw new Error("fallback requires at least one executor");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
execute(request, ctx) {
|
|
59
|
+
return runFallback(
|
|
60
|
+
(executor, index) => executor.execute(request, childContext(ctx, index + 1)),
|
|
61
|
+
executors,
|
|
62
|
+
options,
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
stream(request, ctx) {
|
|
67
|
+
return streamBuffered(() =>
|
|
68
|
+
runFallback(
|
|
69
|
+
(executor, index) => collect(executor.stream(request, childContext(ctx, index + 1))),
|
|
70
|
+
executors,
|
|
71
|
+
options,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function goal(options: GoalOptions): Executor {
|
|
79
|
+
const maxIterations = positiveInteger(options.maxIterations ?? 3, "goal maxIterations");
|
|
80
|
+
const executeGoal: Executor["execute"] = async (request, ctx) => {
|
|
81
|
+
let current = request;
|
|
82
|
+
let answer = request;
|
|
83
|
+
|
|
84
|
+
for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
|
|
85
|
+
answer = await options.draft.execute(current, childContext(ctx, attempt));
|
|
86
|
+
const review = await options.review.execute(answer, childContext(ctx, attempt));
|
|
87
|
+
if (await options.satisfied(review, attempt)) {
|
|
88
|
+
return answer;
|
|
89
|
+
}
|
|
90
|
+
if (attempt < maxIterations) {
|
|
91
|
+
current = await options.refine(current, answer, review, attempt);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return answer;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
execute: executeGoal,
|
|
100
|
+
|
|
101
|
+
stream(request, ctx) {
|
|
102
|
+
return streamBuffered(async () => [await executeGoal(request, ctx)]);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function runRetry<T>(
|
|
108
|
+
operation: (attempt: number) => Promise<T>,
|
|
109
|
+
attempts: number,
|
|
110
|
+
options: RetryOptions,
|
|
111
|
+
): Promise<T> {
|
|
112
|
+
let lastError: unknown;
|
|
113
|
+
|
|
114
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
115
|
+
try {
|
|
116
|
+
return await operation(attempt);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
lastError = error;
|
|
119
|
+
const canRetry = attempt < attempts && (await shouldContinue(error, attempt, options.shouldRetry));
|
|
120
|
+
if (!canRetry) {
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
await options.onRetry?.(error, attempt);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw lastError;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runFallback<T>(
|
|
131
|
+
operation: (executor: Executor, index: number) => Promise<T>,
|
|
132
|
+
executors: readonly Executor[],
|
|
133
|
+
options: FallbackOptions,
|
|
134
|
+
): Promise<T> {
|
|
135
|
+
let lastError: unknown;
|
|
136
|
+
|
|
137
|
+
for (const [index, executor] of executors.entries()) {
|
|
138
|
+
try {
|
|
139
|
+
return await operation(executor, index);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
lastError = error;
|
|
142
|
+
const canFallback =
|
|
143
|
+
index < executors.length - 1 &&
|
|
144
|
+
(await shouldContinue(error, index, options.shouldFallback));
|
|
145
|
+
if (!canFallback) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
await options.onFallback?.(error, index);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw lastError;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function shouldContinue(
|
|
156
|
+
error: unknown,
|
|
157
|
+
attempt: number,
|
|
158
|
+
predicate?: (error: unknown, attempt: number) => boolean | Promise<boolean>,
|
|
159
|
+
): Promise<boolean> {
|
|
160
|
+
return predicate ? predicate(error, attempt) : true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function collect(stream: AsyncIterable<Program>): Promise<Program[]> {
|
|
164
|
+
const chunks: Program[] = [];
|
|
165
|
+
for await (const chunk of stream) {
|
|
166
|
+
chunks.push(chunk);
|
|
167
|
+
}
|
|
168
|
+
return chunks;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function* streamBuffered(load: () => Promise<Program[]>): AsyncIterable<Program> {
|
|
172
|
+
for (const chunk of await load()) {
|
|
173
|
+
yield chunk;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function childContext(ctx: ExecutorContext, attempt: number): ExecutorContext {
|
|
178
|
+
return {
|
|
179
|
+
...ctx,
|
|
180
|
+
executionId: `${ctx.executionId}:${attempt}`,
|
|
181
|
+
parentExecutionId: ctx.executionId,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function positiveInteger(value: number, label: string): number {
|
|
186
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
187
|
+
throw new Error(`${label} must be a positive integer`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { wrap } from "./wrap.ts";
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
appendInternalDraft,
|
|
5
|
+
createTwoPassExecutor,
|
|
6
|
+
INTERNAL_DRAFT_CALL_ID,
|
|
7
|
+
INTERNAL_DRAFT_TOOL_NAME,
|
|
8
|
+
invokeModel,
|
|
9
|
+
streamModel,
|
|
10
|
+
} from "./twoPassExecutor.ts";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
InternalDraft,
|
|
14
|
+
TwoPassExecutorOptions,
|
|
15
|
+
} from "./twoPassExecutor.ts";
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callData,
|
|
3
|
+
contentText,
|
|
4
|
+
deltaText,
|
|
5
|
+
finishReason,
|
|
6
|
+
hasToolDelta,
|
|
7
|
+
insertBefore,
|
|
8
|
+
lastMessageRole,
|
|
9
|
+
messageText,
|
|
10
|
+
messages,
|
|
11
|
+
removeRange,
|
|
12
|
+
setModel,
|
|
13
|
+
type Program,
|
|
14
|
+
} from "@neutrome/lil-engine";
|
|
15
|
+
import { streamReasoningDelta } from "../output.ts";
|
|
16
|
+
import {
|
|
17
|
+
appendToolInteraction,
|
|
18
|
+
makeMessage,
|
|
19
|
+
prependSystemPrompt,
|
|
20
|
+
} from "../synthetic/index.ts";
|
|
21
|
+
import type { Executor, ExecutorContext, InvokeOptions } from "../types.ts";
|
|
22
|
+
|
|
23
|
+
export const INTERNAL_DRAFT_TOOL_NAME = "knowledge";
|
|
24
|
+
export const INTERNAL_DRAFT_CALL_ID = "knowledge_0";
|
|
25
|
+
|
|
26
|
+
export type InternalDraft =
|
|
27
|
+
| string
|
|
28
|
+
| readonly string[]
|
|
29
|
+
| readonly { text: string }[];
|
|
30
|
+
|
|
31
|
+
export type TwoPassExecutorOptions = {
|
|
32
|
+
draftModel: string;
|
|
33
|
+
finalModel: string;
|
|
34
|
+
draftSystemPrompt?: string;
|
|
35
|
+
finalSystemPrompt?: string;
|
|
36
|
+
reasoningIntro?: string;
|
|
37
|
+
reasoningSeparator?: string;
|
|
38
|
+
buildFinalRequest?: (request: Program, draft: string) => Program;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function invokeModel(
|
|
42
|
+
ctx: ExecutorContext,
|
|
43
|
+
request: Program,
|
|
44
|
+
model: string,
|
|
45
|
+
options?: InvokeOptions,
|
|
46
|
+
): Promise<Program> {
|
|
47
|
+
return ctx.invoke(setModel(request, model), options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function* streamModel(
|
|
51
|
+
ctx: ExecutorContext,
|
|
52
|
+
request: Program,
|
|
53
|
+
model: string,
|
|
54
|
+
options?: InvokeOptions,
|
|
55
|
+
): AsyncIterable<Program> {
|
|
56
|
+
yield* ctx.invokeStream(setModel(request, model), options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function appendInternalDraft(
|
|
60
|
+
request: Program,
|
|
61
|
+
draft: InternalDraft,
|
|
62
|
+
options: { callId?: string } = {},
|
|
63
|
+
): Program {
|
|
64
|
+
const callId = options.callId ?? INTERNAL_DRAFT_CALL_ID;
|
|
65
|
+
const drafts = normalizeDrafts(draft);
|
|
66
|
+
if (drafts.length === 0) {
|
|
67
|
+
return request;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return drafts.reduce(
|
|
71
|
+
(program, text, index) =>
|
|
72
|
+
appendToolInteraction(program, {
|
|
73
|
+
callId: drafts.length === 1 ? callId : `${callId}_${index}`,
|
|
74
|
+
name: INTERNAL_DRAFT_TOOL_NAME,
|
|
75
|
+
args: {},
|
|
76
|
+
result: text,
|
|
77
|
+
}),
|
|
78
|
+
request,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createTwoPassExecutor(options: TwoPassExecutorOptions): Executor {
|
|
83
|
+
const reasoningIntro = options.reasoningIntro ?? "Let me think...\n\n";
|
|
84
|
+
const reasoningSeparator = options.reasoningSeparator ?? "\n\n";
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
async execute(request, ctx) {
|
|
88
|
+
const draftRequest = buildDraftRequest(options, request);
|
|
89
|
+
if (lastMessageRole(request) === "tool") {
|
|
90
|
+
return invokeModel(ctx, draftRequest, options.draftModel);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const draftResponse = await invokeModel(ctx, draftRequest, options.draftModel);
|
|
94
|
+
if (callData(draftResponse).length > 0) {
|
|
95
|
+
return draftResponse;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return invokeModel(
|
|
99
|
+
ctx,
|
|
100
|
+
buildFinalRequest(options, request, contentText(draftResponse)),
|
|
101
|
+
options.finalModel,
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async *stream(request, ctx) {
|
|
106
|
+
const draftRequest = buildDraftRequest(options, request);
|
|
107
|
+
if (lastMessageRole(request) === "tool") {
|
|
108
|
+
yield* streamModel(ctx, draftRequest, options.draftModel);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let transcript = "";
|
|
113
|
+
let emittedReasoning = false;
|
|
114
|
+
let toolMode = false;
|
|
115
|
+
const draftStream = streamModel(ctx, draftRequest, options.draftModel);
|
|
116
|
+
|
|
117
|
+
for await (const chunk of draftStream) {
|
|
118
|
+
if (ctx.signal.aborted) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (toolMode) {
|
|
123
|
+
yield chunk;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const toolRequested = hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
|
|
128
|
+
if (toolRequested) {
|
|
129
|
+
toolMode = true;
|
|
130
|
+
yield chunk;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const text = deltaText(chunk);
|
|
135
|
+
if (!text) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
transcript += text;
|
|
140
|
+
if (!emittedReasoning) {
|
|
141
|
+
emittedReasoning = true;
|
|
142
|
+
yield streamReasoningDelta(reasoningIntro);
|
|
143
|
+
}
|
|
144
|
+
yield streamReasoningDelta(text);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (toolMode || ctx.signal.aborted) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (emittedReasoning && reasoningSeparator) {
|
|
152
|
+
yield streamReasoningDelta(reasoningSeparator);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
yield* streamModel(
|
|
156
|
+
ctx,
|
|
157
|
+
buildFinalRequest(options, request, transcript),
|
|
158
|
+
options.finalModel,
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildDraftRequest(options: TwoPassExecutorOptions, request: Program): Program {
|
|
165
|
+
if (!options.draftSystemPrompt) {
|
|
166
|
+
return request;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const systemSpans = messages(request).filter((span) => span.role === "system");
|
|
170
|
+
const previousSystemPrompt = systemSpans
|
|
171
|
+
.map((span) => messageText(request, span).trim())
|
|
172
|
+
.filter((text) => text.length > 0)
|
|
173
|
+
.join("\n\n");
|
|
174
|
+
|
|
175
|
+
let draft = request;
|
|
176
|
+
for (const span of systemSpans.slice().reverse()) {
|
|
177
|
+
draft = removeRange(draft, span.start, span.end);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
draft = prependSystemPrompt(draft, options.draftSystemPrompt);
|
|
181
|
+
|
|
182
|
+
if (!previousSystemPrompt) {
|
|
183
|
+
return draft;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const firstNonSystem = messages(draft).find((span) => span.role !== "system");
|
|
187
|
+
return insertBefore(
|
|
188
|
+
draft,
|
|
189
|
+
firstNonSystem?.start ?? draft.code.length,
|
|
190
|
+
makeMessage("user", previousSystemPrompt).code,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildFinalRequest(
|
|
195
|
+
options: TwoPassExecutorOptions,
|
|
196
|
+
request: Program,
|
|
197
|
+
draft: string,
|
|
198
|
+
): Program {
|
|
199
|
+
if (options.buildFinalRequest) {
|
|
200
|
+
return options.buildFinalRequest(request, draft);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const withDraft = appendInternalDraft(request, draft);
|
|
204
|
+
return options.finalSystemPrompt
|
|
205
|
+
? prependSystemPrompt(withDraft, options.finalSystemPrompt)
|
|
206
|
+
: withDraft;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeDrafts(draft: InternalDraft): string[] {
|
|
210
|
+
const drafts = Array.isArray(draft)
|
|
211
|
+
? draft.map((item) => typeof item === "string" ? item : item.text)
|
|
212
|
+
: [draft];
|
|
213
|
+
return drafts.map((text) => text.trim()).filter((text) => text.length > 0);
|
|
214
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { setModel } from "@neutrome/lil-engine";
|
|
2
|
+
import { Executor } from "../types";
|
|
3
|
+
|
|
4
|
+
export function wrap(model: string): Executor {
|
|
5
|
+
return {
|
|
6
|
+
execute: (program, ctx) => ctx.invoke(setModel(program, model)),
|
|
7
|
+
stream: (request, ctx) => ctx.invokeStream(setModel(request, model)),
|
|
8
|
+
};
|
|
9
|
+
}
|
package/src/observe.ts
CHANGED
|
@@ -1,2 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
cloneProgram,
|
|
3
|
+
deltaText,
|
|
4
|
+
finishReason,
|
|
5
|
+
hasToolDelta,
|
|
6
|
+
type Program,
|
|
7
|
+
} from "@neutrome/lil-engine";
|
|
8
|
+
|
|
9
|
+
export type StreamObservationHooks = {
|
|
10
|
+
onTextStart?: () => void;
|
|
11
|
+
onTextChunk?: (text: string, chunk: Program) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ObservedExecution =
|
|
15
|
+
| {
|
|
16
|
+
mode: "text";
|
|
17
|
+
transcript: string;
|
|
18
|
+
emittedText: boolean;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
mode: "tool";
|
|
22
|
+
transcript: string;
|
|
23
|
+
emittedText: boolean;
|
|
24
|
+
chunks: Program[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function observeExecutionStream(
|
|
28
|
+
source: AsyncIterable<Program>,
|
|
29
|
+
hooks: StreamObservationHooks = {},
|
|
30
|
+
): Promise<ObservedExecution> {
|
|
31
|
+
const pending: Program[] = [];
|
|
32
|
+
const passthrough: Program[] = [];
|
|
33
|
+
let transcript = "";
|
|
34
|
+
let mode: "pending" | "text" | "tool" = "pending";
|
|
35
|
+
let emittedText = false;
|
|
36
|
+
|
|
37
|
+
for await (const chunk of source) {
|
|
38
|
+
const text = deltaText(chunk);
|
|
39
|
+
const toolRequested = hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
|
|
40
|
+
|
|
41
|
+
if (mode === "pending") {
|
|
42
|
+
const clone = cloneProgram(chunk);
|
|
43
|
+
pending.push(clone);
|
|
44
|
+
|
|
45
|
+
if (toolRequested) {
|
|
46
|
+
mode = "tool";
|
|
47
|
+
passthrough.push(...pending.splice(0));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!text) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
mode = "text";
|
|
56
|
+
emittedText = true;
|
|
57
|
+
hooks.onTextStart?.();
|
|
58
|
+
for (const pendingChunk of pending.splice(0)) {
|
|
59
|
+
const pendingText = deltaText(pendingChunk);
|
|
60
|
+
if (pendingText) {
|
|
61
|
+
transcript += pendingText;
|
|
62
|
+
hooks.onTextChunk?.(pendingText, pendingChunk);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (mode === "tool") {
|
|
69
|
+
passthrough.push(cloneProgram(chunk));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (toolRequested) {
|
|
74
|
+
mode = "tool";
|
|
75
|
+
passthrough.push(cloneProgram(chunk));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (text) {
|
|
80
|
+
if (!emittedText) {
|
|
81
|
+
emittedText = true;
|
|
82
|
+
hooks.onTextStart?.();
|
|
83
|
+
}
|
|
84
|
+
transcript += text;
|
|
85
|
+
hooks.onTextChunk?.(text, chunk);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (mode === "tool") {
|
|
90
|
+
return { mode: "tool", transcript, emittedText, chunks: passthrough };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { mode: "text", transcript, emittedText };
|
|
94
|
+
}
|
package/src/output.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createProgram,
|
|
3
|
+
Opcode,
|
|
4
|
+
streamEnd,
|
|
5
|
+
streamStart,
|
|
6
|
+
streamTextDelta,
|
|
7
|
+
type Program,
|
|
8
|
+
} from "@neutrome/lil-engine";
|
|
2
9
|
import type { OutputSink } from "./types.ts";
|
|
3
10
|
|
|
4
11
|
export async function writeReasoning(sink: OutputSink, text: string): Promise<void> {
|
|
5
|
-
await sink.write(
|
|
12
|
+
await sink.write(streamReasoningDelta(text));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function streamTextResponse(text: string): Program[] {
|
|
16
|
+
return [streamStart(), streamTextDelta(text), streamEnd()];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function streamReasoningDelta(text: string): Program {
|
|
20
|
+
return createProgram({
|
|
6
21
|
code: [
|
|
7
22
|
{ opcode: Opcode.STREAM_THINK_DELTA, value: { kind: "string", value: text } },
|
|
8
23
|
],
|
|
9
|
-
})
|
|
24
|
+
});
|
|
10
25
|
}
|