@neutrome/lilsdk 0.3.5
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 +31 -0
- package/package.json +27 -0
- package/src/index.ts +14 -0
- package/src/loops/index.ts +266 -0
- package/src/managed/index.ts +18 -0
- package/src/managed/target-executor.ts +12 -0
- package/src/managed/two-pass-request.ts +164 -0
- package/src/managed/twoPassExecutor.ts +200 -0
- package/src/observe.ts +95 -0
- package/src/output.ts +31 -0
- package/src/primitives/index.ts +154 -0
- package/src/stream/index.ts +8 -0
- package/src/synthetic/index.ts +134 -0
- package/src/tools-support.ts +108 -0
- package/src/tools.ts +279 -0
- package/src/types.ts +101 -0
- package/test/lilsdk-ts.test.ts +660 -0
- package/test/tools.test.ts +452 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callData,
|
|
3
|
+
contentText,
|
|
4
|
+
deltaText,
|
|
5
|
+
finishReason,
|
|
6
|
+
hasToolDelta,
|
|
7
|
+
lastMessageRole,
|
|
8
|
+
setModel,
|
|
9
|
+
type Program,
|
|
10
|
+
} from "@neutrome/lil-engine";
|
|
11
|
+
import { streamReasoningDelta } from "../output.ts";
|
|
12
|
+
import type { Executor, ExecutorContext, InvokeOptions } from "../types.ts";
|
|
13
|
+
import {
|
|
14
|
+
appendInternalDraft,
|
|
15
|
+
buildDraftRequest,
|
|
16
|
+
buildFinalRequest,
|
|
17
|
+
INTERNAL_DRAFT_CALL_ID,
|
|
18
|
+
INTERNAL_DRAFT_TOOL_NAME,
|
|
19
|
+
validateMaxContextLength,
|
|
20
|
+
type InternalDraft,
|
|
21
|
+
type TwoPassRequestOptions,
|
|
22
|
+
type TwoPassSettings,
|
|
23
|
+
type TwoPassSettingsResolver,
|
|
24
|
+
} from "./two-pass-request.ts";
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
InternalDraft,
|
|
28
|
+
TwoPassSettings,
|
|
29
|
+
TwoPassSettingsResolver,
|
|
30
|
+
} from "./two-pass-request.ts";
|
|
31
|
+
export {
|
|
32
|
+
appendInternalDraft,
|
|
33
|
+
INTERNAL_DRAFT_CALL_ID,
|
|
34
|
+
INTERNAL_DRAFT_TOOL_NAME,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ModelExecutor = string | Executor;
|
|
38
|
+
|
|
39
|
+
export type TwoPassExecutorOptions = TwoPassRequestOptions & {
|
|
40
|
+
draftModel: ModelExecutor;
|
|
41
|
+
finalModel: ModelExecutor;
|
|
42
|
+
resolveDraftSettings?: TwoPassSettingsResolver;
|
|
43
|
+
resolveFinalSettings?: TwoPassSettingsResolver;
|
|
44
|
+
reasoningIntro?: string;
|
|
45
|
+
reasoningSeparator?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function invokeExecutor(
|
|
49
|
+
ctx: ExecutorContext,
|
|
50
|
+
request: Program,
|
|
51
|
+
executor: ModelExecutor,
|
|
52
|
+
options?: InvokeOptions,
|
|
53
|
+
): Promise<Program> {
|
|
54
|
+
return typeof executor === "string"
|
|
55
|
+
? ctx.invoke(setModel(request, executor), options)
|
|
56
|
+
: executor.execute(request, ctx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function* streamExecutor(
|
|
60
|
+
ctx: ExecutorContext,
|
|
61
|
+
request: Program,
|
|
62
|
+
executor: ModelExecutor,
|
|
63
|
+
options?: InvokeOptions,
|
|
64
|
+
): AsyncIterable<Program> {
|
|
65
|
+
if (typeof executor === "string") {
|
|
66
|
+
yield* ctx.invokeStream(setModel(request, executor), options);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
yield* executor.stream(request, ctx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createTwoPassExecutor(
|
|
73
|
+
options: TwoPassExecutorOptions,
|
|
74
|
+
): Executor {
|
|
75
|
+
const reasoningIntro = options.reasoningIntro ?? "Let me think...\n\n";
|
|
76
|
+
const reasoningSeparator = options.reasoningSeparator ?? "\n\n";
|
|
77
|
+
validateMaxContextLength(options.maxTotalContextLength);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
async execute(request, ctx) {
|
|
81
|
+
const settings = await resolveSettings(options, request, ctx);
|
|
82
|
+
if (!settings.draft) {
|
|
83
|
+
return invokeFinal(options, request, ctx, settings.final!);
|
|
84
|
+
}
|
|
85
|
+
const draftRequest = buildDraftRequest(request, settings.draft);
|
|
86
|
+
if (!settings.final || lastMessageRole(request) === "tool") {
|
|
87
|
+
return invokeExecutor(ctx, draftRequest, options.draftModel);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const draftResponse = await invokeExecutor(
|
|
91
|
+
ctx,
|
|
92
|
+
draftRequest,
|
|
93
|
+
options.draftModel,
|
|
94
|
+
);
|
|
95
|
+
if (callData(draftResponse).length > 0) return draftResponse;
|
|
96
|
+
|
|
97
|
+
return invokeFinal(
|
|
98
|
+
options,
|
|
99
|
+
request,
|
|
100
|
+
ctx,
|
|
101
|
+
settings.final,
|
|
102
|
+
contentText(draftResponse),
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async *stream(request, ctx) {
|
|
107
|
+
const settings = await resolveSettings(options, request, ctx);
|
|
108
|
+
if (!settings.draft) {
|
|
109
|
+
yield* streamFinal(options, request, ctx, settings.final!);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const draftRequest = buildDraftRequest(request, settings.draft);
|
|
113
|
+
if (!settings.final || lastMessageRole(request) === "tool") {
|
|
114
|
+
yield* streamExecutor(ctx, draftRequest, options.draftModel);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let transcript = "";
|
|
119
|
+
let emittedReasoning = false;
|
|
120
|
+
let toolMode = false;
|
|
121
|
+
for await (const chunk of streamExecutor(
|
|
122
|
+
ctx,
|
|
123
|
+
draftRequest,
|
|
124
|
+
options.draftModel,
|
|
125
|
+
)) {
|
|
126
|
+
if (ctx.signal.aborted) return;
|
|
127
|
+
if (toolMode) {
|
|
128
|
+
yield chunk;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (hasToolDelta(chunk) || finishReason(chunk) === "tool_calls") {
|
|
132
|
+
toolMode = true;
|
|
133
|
+
yield chunk;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const text = deltaText(chunk);
|
|
137
|
+
if (!text) continue;
|
|
138
|
+
transcript += text;
|
|
139
|
+
if (!emittedReasoning) {
|
|
140
|
+
emittedReasoning = true;
|
|
141
|
+
yield streamReasoningDelta(reasoningIntro);
|
|
142
|
+
}
|
|
143
|
+
yield streamReasoningDelta(text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (toolMode || ctx.signal.aborted) return;
|
|
147
|
+
if (emittedReasoning && reasoningSeparator) {
|
|
148
|
+
yield streamReasoningDelta(reasoningSeparator);
|
|
149
|
+
}
|
|
150
|
+
yield* streamFinal(options, request, ctx, settings.final, transcript);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function resolveSettings(
|
|
156
|
+
options: TwoPassExecutorOptions,
|
|
157
|
+
request: Program,
|
|
158
|
+
ctx: ExecutorContext,
|
|
159
|
+
): Promise<{ draft: TwoPassSettings | null; final: TwoPassSettings | null }> {
|
|
160
|
+
const [draft, final] = await Promise.all([
|
|
161
|
+
options.resolveDraftSettings
|
|
162
|
+
? options.resolveDraftSettings(request, ctx)
|
|
163
|
+
: {},
|
|
164
|
+
options.resolveFinalSettings
|
|
165
|
+
? options.resolveFinalSettings(request, ctx)
|
|
166
|
+
: {},
|
|
167
|
+
]);
|
|
168
|
+
if (!draft && !final) {
|
|
169
|
+
throw new Error("Two-pass executor requires at least one enabled pass");
|
|
170
|
+
}
|
|
171
|
+
return { draft, final };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function invokeFinal(
|
|
175
|
+
options: TwoPassExecutorOptions,
|
|
176
|
+
request: Program,
|
|
177
|
+
ctx: ExecutorContext,
|
|
178
|
+
settings: TwoPassSettings,
|
|
179
|
+
draft = "",
|
|
180
|
+
): Promise<Program> {
|
|
181
|
+
return invokeExecutor(
|
|
182
|
+
ctx,
|
|
183
|
+
buildFinalRequest(options, request, settings, draft),
|
|
184
|
+
options.finalModel,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function* streamFinal(
|
|
189
|
+
options: TwoPassExecutorOptions,
|
|
190
|
+
request: Program,
|
|
191
|
+
ctx: ExecutorContext,
|
|
192
|
+
settings: TwoPassSettings,
|
|
193
|
+
draft = "",
|
|
194
|
+
): AsyncIterable<Program> {
|
|
195
|
+
yield* streamExecutor(
|
|
196
|
+
ctx,
|
|
197
|
+
buildFinalRequest(options, request, settings, draft),
|
|
198
|
+
options.finalModel,
|
|
199
|
+
);
|
|
200
|
+
}
|
package/src/observe.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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 =
|
|
40
|
+
hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
|
|
41
|
+
|
|
42
|
+
if (mode === "pending") {
|
|
43
|
+
const clone = cloneProgram(chunk);
|
|
44
|
+
pending.push(clone);
|
|
45
|
+
|
|
46
|
+
if (toolRequested) {
|
|
47
|
+
mode = "tool";
|
|
48
|
+
passthrough.push(...pending.splice(0));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!text) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mode = "text";
|
|
57
|
+
emittedText = true;
|
|
58
|
+
hooks.onTextStart?.();
|
|
59
|
+
for (const pendingChunk of pending.splice(0)) {
|
|
60
|
+
const pendingText = deltaText(pendingChunk);
|
|
61
|
+
if (pendingText) {
|
|
62
|
+
transcript += pendingText;
|
|
63
|
+
hooks.onTextChunk?.(pendingText, pendingChunk);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (mode === "tool") {
|
|
70
|
+
passthrough.push(cloneProgram(chunk));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (toolRequested) {
|
|
75
|
+
mode = "tool";
|
|
76
|
+
passthrough.push(cloneProgram(chunk));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (text) {
|
|
81
|
+
if (!emittedText) {
|
|
82
|
+
emittedText = true;
|
|
83
|
+
hooks.onTextStart?.();
|
|
84
|
+
}
|
|
85
|
+
transcript += text;
|
|
86
|
+
hooks.onTextChunk?.(text, chunk);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (mode === "tool") {
|
|
91
|
+
return { mode: "tool", transcript, emittedText, chunks: passthrough };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { mode: "text", transcript, emittedText };
|
|
95
|
+
}
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createProgram,
|
|
3
|
+
Opcode,
|
|
4
|
+
streamEnd,
|
|
5
|
+
streamStart,
|
|
6
|
+
streamTextDelta,
|
|
7
|
+
type Program,
|
|
8
|
+
} from "@neutrome/lil-engine";
|
|
9
|
+
import type { OutputSink } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export async function writeReasoning(
|
|
12
|
+
sink: OutputSink,
|
|
13
|
+
text: string,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
await sink.write(streamReasoningDelta(text));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function streamTextResponse(text: string): Program[] {
|
|
19
|
+
return [streamStart(), streamTextDelta(text), streamEnd()];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function streamReasoningDelta(text: string): Program {
|
|
23
|
+
return createProgram({
|
|
24
|
+
code: [
|
|
25
|
+
{
|
|
26
|
+
opcode: Opcode.STREAM_THINK_DELTA,
|
|
27
|
+
value: { kind: "string", value: text },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneInstruction,
|
|
3
|
+
type Instruction,
|
|
4
|
+
type Program,
|
|
5
|
+
} from "@neutrome/lil-engine";
|
|
6
|
+
|
|
7
|
+
export function indexOf(program: Program, needle: Instruction): number {
|
|
8
|
+
return program.code.findIndex((instruction) =>
|
|
9
|
+
instructionEquals(instruction, needle),
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function has(program: Program, needle: Instruction): boolean {
|
|
14
|
+
return indexOf(program, needle) >= 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function find(
|
|
18
|
+
program: Program,
|
|
19
|
+
needle: Instruction,
|
|
20
|
+
): Instruction | undefined {
|
|
21
|
+
const i = indexOf(program, needle);
|
|
22
|
+
return i >= 0 ? cloneInstruction(program.code[i]!) : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function walk(
|
|
26
|
+
program: Program,
|
|
27
|
+
callback: (instruction: Instruction, index: number, program: Program) => void,
|
|
28
|
+
): void {
|
|
29
|
+
program.code.forEach((instruction, index) =>
|
|
30
|
+
callback(instruction, index, program),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function map(
|
|
35
|
+
program: Program,
|
|
36
|
+
callback: (
|
|
37
|
+
instruction: Instruction,
|
|
38
|
+
index: number,
|
|
39
|
+
program: Program,
|
|
40
|
+
) => Instruction,
|
|
41
|
+
): Program {
|
|
42
|
+
return {
|
|
43
|
+
code: program.code.map((instruction, index) =>
|
|
44
|
+
cloneInstruction(callback(instruction, index, program)),
|
|
45
|
+
),
|
|
46
|
+
buffers: cloneBuffers(program.buffers),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function filter(
|
|
51
|
+
program: Program,
|
|
52
|
+
callback: (
|
|
53
|
+
instruction: Instruction,
|
|
54
|
+
index: number,
|
|
55
|
+
program: Program,
|
|
56
|
+
) => boolean,
|
|
57
|
+
): Program {
|
|
58
|
+
return {
|
|
59
|
+
code: program.code
|
|
60
|
+
.filter((instruction, index) => callback(instruction, index, program))
|
|
61
|
+
.map(cloneInstruction),
|
|
62
|
+
buffers: cloneBuffers(program.buffers),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function reduce<T>(
|
|
67
|
+
program: Program,
|
|
68
|
+
callback: (
|
|
69
|
+
accumulator: T,
|
|
70
|
+
instruction: Instruction,
|
|
71
|
+
index: number,
|
|
72
|
+
program: Program,
|
|
73
|
+
) => T,
|
|
74
|
+
initialValue: T,
|
|
75
|
+
): T {
|
|
76
|
+
let accumulator = initialValue;
|
|
77
|
+
program.code.forEach((instruction, index) => {
|
|
78
|
+
accumulator = callback(accumulator, instruction, index, program);
|
|
79
|
+
});
|
|
80
|
+
return accumulator;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function all(
|
|
84
|
+
program: Program,
|
|
85
|
+
callback: (
|
|
86
|
+
instruction: Instruction,
|
|
87
|
+
index: number,
|
|
88
|
+
program: Program,
|
|
89
|
+
) => boolean,
|
|
90
|
+
): boolean {
|
|
91
|
+
for (const [index, instruction] of program.code.entries()) {
|
|
92
|
+
if (!callback(instruction, index, program)) return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function any(
|
|
98
|
+
program: Program,
|
|
99
|
+
callback: (
|
|
100
|
+
instruction: Instruction,
|
|
101
|
+
index: number,
|
|
102
|
+
program: Program,
|
|
103
|
+
) => boolean,
|
|
104
|
+
): boolean {
|
|
105
|
+
for (const [index, instruction] of program.code.entries()) {
|
|
106
|
+
if (callback(instruction, index, program)) return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function instructionEquals(left: Instruction, right: Instruction): boolean {
|
|
112
|
+
if (left.opcode !== right.opcode || left.value.kind !== right.value.kind) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
switch (left.value.kind) {
|
|
117
|
+
case "none":
|
|
118
|
+
return true;
|
|
119
|
+
case "string":
|
|
120
|
+
case "float":
|
|
121
|
+
case "int":
|
|
122
|
+
case "ref":
|
|
123
|
+
return (
|
|
124
|
+
right.value.kind === left.value.kind &&
|
|
125
|
+
left.value.value === right.value.value
|
|
126
|
+
);
|
|
127
|
+
case "json":
|
|
128
|
+
return (
|
|
129
|
+
right.value.kind === "json" &&
|
|
130
|
+
bytesEqual(left.value.value, right.value.value)
|
|
131
|
+
);
|
|
132
|
+
case "key_string":
|
|
133
|
+
return (
|
|
134
|
+
right.value.kind === "key_string" &&
|
|
135
|
+
left.value.key === right.value.key &&
|
|
136
|
+
left.value.value === right.value.value
|
|
137
|
+
);
|
|
138
|
+
case "key_json":
|
|
139
|
+
return (
|
|
140
|
+
right.value.kind === "key_json" &&
|
|
141
|
+
left.value.key === right.value.key &&
|
|
142
|
+
bytesEqual(left.value.value, right.value.value)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function bytesEqual(left: Uint8Array, right: Uint8Array): boolean {
|
|
148
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
149
|
+
return left.every((value, index) => value === right[index]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cloneBuffers(buffers: Uint8Array[]): Uint8Array[] {
|
|
153
|
+
return buffers.map((buffer) => buffer.slice());
|
|
154
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addTool,
|
|
3
|
+
appendInstructions,
|
|
4
|
+
createProgram,
|
|
5
|
+
insertBefore,
|
|
6
|
+
messages,
|
|
7
|
+
Opcode,
|
|
8
|
+
setModel,
|
|
9
|
+
type Instruction,
|
|
10
|
+
type Opcode as OpcodeValue,
|
|
11
|
+
type Program,
|
|
12
|
+
} from "@neutrome/lil-engine";
|
|
13
|
+
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
|
|
16
|
+
export type TextMessageRole = "system" | "user" | "assistant";
|
|
17
|
+
|
|
18
|
+
export type ToolInteraction = {
|
|
19
|
+
callId: string;
|
|
20
|
+
name: string;
|
|
21
|
+
args?: unknown;
|
|
22
|
+
result: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createModelProgram(model: string): Program {
|
|
26
|
+
return setModel(createProgram(), model);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function makeMessage(role: TextMessageRole, text: string): Program {
|
|
30
|
+
return createProgram({ code: buildMessage(role, text) });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function prependSystemPrompt(program: Program, text: string): Program {
|
|
34
|
+
const systemMessage = buildMessage("system", text);
|
|
35
|
+
const spans = messages(program);
|
|
36
|
+
if (spans.length === 0) {
|
|
37
|
+
return appendInstructions(program, systemMessage);
|
|
38
|
+
}
|
|
39
|
+
const firstSpan = spans[0];
|
|
40
|
+
if (!firstSpan) return appendInstructions(program, systemMessage);
|
|
41
|
+
return insertBefore(program, firstSpan.start, systemMessage);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function appendUserMessage(program: Program, text: string): Program {
|
|
45
|
+
return appendInstructions(program, buildMessage("user", text));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function appendAssistantMessage(
|
|
49
|
+
program: Program,
|
|
50
|
+
text: string,
|
|
51
|
+
): Program {
|
|
52
|
+
return appendInstructions(program, buildMessage("assistant", text));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function makeToolCall(
|
|
56
|
+
name: string,
|
|
57
|
+
args: unknown = {},
|
|
58
|
+
callId: string = crypto.randomUUID(),
|
|
59
|
+
): Program {
|
|
60
|
+
return createProgram({
|
|
61
|
+
code: [
|
|
62
|
+
{ opcode: Opcode.MSG_START, value: { kind: "none" } },
|
|
63
|
+
{ opcode: Opcode.ROLE_AST, value: { kind: "none" } },
|
|
64
|
+
{ opcode: Opcode.CALL_START, value: { kind: "string", value: callId } },
|
|
65
|
+
{ opcode: Opcode.CALL_NAME, value: { kind: "string", value: name } },
|
|
66
|
+
{
|
|
67
|
+
opcode: Opcode.CALL_ARGS,
|
|
68
|
+
value: { kind: "json", value: encodeJson(args) },
|
|
69
|
+
},
|
|
70
|
+
{ opcode: Opcode.CALL_END, value: { kind: "none" } },
|
|
71
|
+
{ opcode: Opcode.MSG_END, value: { kind: "none" } },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function makeToolResponse(callId: string, result: unknown): Program {
|
|
77
|
+
return createProgram({ code: buildToolResponse(callId, result) });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function appendToolInteraction(
|
|
81
|
+
program: Program,
|
|
82
|
+
interaction: ToolInteraction,
|
|
83
|
+
): Program {
|
|
84
|
+
return appendInstructions(program, [
|
|
85
|
+
...makeToolCall(
|
|
86
|
+
interaction.name,
|
|
87
|
+
interaction.args ?? {},
|
|
88
|
+
interaction.callId,
|
|
89
|
+
).code,
|
|
90
|
+
...buildToolResponse(interaction.callId, interaction.result),
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildMessage(role: TextMessageRole, text: string): Instruction[] {
|
|
95
|
+
return [
|
|
96
|
+
{ opcode: Opcode.MSG_START, value: { kind: "none" } },
|
|
97
|
+
{ opcode: roleOpcode(role), value: { kind: "none" } },
|
|
98
|
+
{ opcode: Opcode.TXT_CHUNK, value: { kind: "string", value: text } },
|
|
99
|
+
{ opcode: Opcode.MSG_END, value: { kind: "none" } },
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildToolResponse(callId: string, result: unknown): Instruction[] {
|
|
104
|
+
return [
|
|
105
|
+
{ opcode: Opcode.MSG_START, value: { kind: "none" } },
|
|
106
|
+
{ opcode: Opcode.ROLE_TOOL, value: { kind: "none" } },
|
|
107
|
+
{ opcode: Opcode.RESULT_START, value: { kind: "string", value: callId } },
|
|
108
|
+
{
|
|
109
|
+
opcode: Opcode.RESULT_DATA,
|
|
110
|
+
value: { kind: "string", value: stringifyResult(result) },
|
|
111
|
+
},
|
|
112
|
+
{ opcode: Opcode.RESULT_END, value: { kind: "none" } },
|
|
113
|
+
{ opcode: Opcode.MSG_END, value: { kind: "none" } },
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function roleOpcode(role: TextMessageRole): OpcodeValue {
|
|
118
|
+
switch (role) {
|
|
119
|
+
case "system":
|
|
120
|
+
return Opcode.ROLE_SYS;
|
|
121
|
+
case "user":
|
|
122
|
+
return Opcode.ROLE_USR;
|
|
123
|
+
case "assistant":
|
|
124
|
+
return Opcode.ROLE_AST;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function encodeJson(value: unknown): Uint8Array {
|
|
129
|
+
return encoder.encode(JSON.stringify(value));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function stringifyResult(result: unknown): string {
|
|
133
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
134
|
+
}
|