@neutrome/lilsdk-ts 0.2.5 → 0.3.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/README.md +7 -4
- package/package.json +2 -2
- package/src/index.ts +3 -3
- package/src/loops/index.ts +50 -19
- package/src/managed/index.ts +1 -1
- package/src/managed/target-executor.ts +12 -0
- package/src/managed/twoPassExecutor.ts +18 -6
- package/src/observe.ts +2 -1
- package/src/output.ts +8 -2
- package/src/primitives/index.ts +45 -11
- package/src/stream/index.ts +5 -1
- package/src/synthetic/index.ts +22 -39
- package/src/tools-support.ts +108 -0
- package/src/tools.ts +144 -228
- package/src/types.ts +15 -7
- package/test/lilsdk-ts.test.ts +266 -102
- package/test/tools.test.ts +287 -94
- package/src/managed/wrap.ts +0 -17
package/README.md
CHANGED
|
@@ -5,7 +5,10 @@ High-level TypeScript SDK for transport-neutral model authoring on top of `@neut
|
|
|
5
5
|
```ts
|
|
6
6
|
import { createTwoPassExecutor, type Executor } from "@neutrome/lilsdk-ts";
|
|
7
7
|
import { retry } from "@neutrome/lilsdk-ts/loops";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
observeExecutionStream,
|
|
10
|
+
writeReasoning,
|
|
11
|
+
} from "@neutrome/lilsdk-ts/stream";
|
|
9
12
|
import { connectTools } from "@neutrome/lilsdk-ts/tools";
|
|
10
13
|
```
|
|
11
14
|
|
|
@@ -13,10 +16,10 @@ This package owns:
|
|
|
13
16
|
|
|
14
17
|
- executor and tool contracts
|
|
15
18
|
- `@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
|
|
17
|
-
- `@neutrome/lilsdk-ts/tools`: `connectTools`
|
|
19
|
+
- `@neutrome/lilsdk-ts/synthetic`: request/message synthesis helpers such as `createModelProgram`, `makeMessage`, `makeToolCall`, `makeToolResponse`, and `appendToolInteraction`
|
|
20
|
+
- `@neutrome/lilsdk-ts/tools`: `connectTools` tool-call loop
|
|
18
21
|
- `@neutrome/lilsdk-ts/stream`: stream observation and output helpers
|
|
19
|
-
- `@neutrome/lilsdk-ts/loops`: `retry`, `fallback`, and `
|
|
22
|
+
- `@neutrome/lilsdk-ts/loops`: `retry`, `fallback`, and execute-only `runGoal` control-flow helpers
|
|
20
23
|
- `@neutrome/lilsdk-ts/managed/twoPassExecutor`: `createTwoPassExecutor` and model invocation helpers
|
|
21
24
|
- multi-pass authoring primitives used by virtual models
|
|
22
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/lilsdk-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.1"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^25.9.3",
|
package/src/index.ts
CHANGED
|
@@ -5,10 +5,10 @@ export type {
|
|
|
5
5
|
InvokeOptions,
|
|
6
6
|
OutputSink,
|
|
7
7
|
ProgramTransform,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
ExecutionEvent,
|
|
9
|
+
ExecutionEventInput,
|
|
10
10
|
Tool,
|
|
11
11
|
TransformCapability,
|
|
12
12
|
TransformContext,
|
|
13
|
-
createSdkEvent,
|
|
14
13
|
} from "./types.ts";
|
|
14
|
+
export { createExecutionEvent } from "./types.ts";
|
package/src/loops/index.ts
CHANGED
|
@@ -8,7 +8,10 @@ export type RetryOptions = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export type FallbackOptions = {
|
|
11
|
-
shouldFallback?: (
|
|
11
|
+
shouldFallback?: (
|
|
12
|
+
error: unknown,
|
|
13
|
+
executorIndex: number,
|
|
14
|
+
) => boolean | Promise<boolean>;
|
|
12
15
|
onFallback?: (error: unknown, executorIndex: number) => void | Promise<void>;
|
|
13
16
|
};
|
|
14
17
|
|
|
@@ -25,7 +28,12 @@ export type GoalOptions = {
|
|
|
25
28
|
maxIterations?: number;
|
|
26
29
|
};
|
|
27
30
|
|
|
28
|
-
export
|
|
31
|
+
export type ExecuteOnly = Pick<Executor, "execute">;
|
|
32
|
+
|
|
33
|
+
export function retry(
|
|
34
|
+
executor: Executor,
|
|
35
|
+
options: RetryOptions = {},
|
|
36
|
+
): Executor {
|
|
29
37
|
const attempts = positiveInteger(options.attempts ?? 2, "retry attempts");
|
|
30
38
|
|
|
31
39
|
return {
|
|
@@ -41,12 +49,22 @@ export function retry(executor: Executor, options: RetryOptions = {}): Executor
|
|
|
41
49
|
let lastError: unknown;
|
|
42
50
|
|
|
43
51
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
52
|
+
let emitted = false;
|
|
44
53
|
try {
|
|
45
|
-
|
|
54
|
+
for await (const chunk of executor.stream(
|
|
55
|
+
request,
|
|
56
|
+
childContext(ctx, attempt),
|
|
57
|
+
)) {
|
|
58
|
+
emitted = true;
|
|
59
|
+
yield chunk;
|
|
60
|
+
}
|
|
46
61
|
return;
|
|
47
62
|
} catch (error) {
|
|
48
63
|
lastError = error;
|
|
49
|
-
|
|
64
|
+
if (emitted) throw error;
|
|
65
|
+
const canRetry =
|
|
66
|
+
attempt < attempts &&
|
|
67
|
+
(await shouldContinue(error, attempt, options.shouldRetry));
|
|
50
68
|
if (!canRetry) {
|
|
51
69
|
throw error;
|
|
52
70
|
}
|
|
@@ -59,7 +77,10 @@ export function retry(executor: Executor, options: RetryOptions = {}): Executor
|
|
|
59
77
|
};
|
|
60
78
|
}
|
|
61
79
|
|
|
62
|
-
export function fallback(
|
|
80
|
+
export function fallback(
|
|
81
|
+
executors: readonly Executor[],
|
|
82
|
+
options: FallbackOptions = {},
|
|
83
|
+
): Executor {
|
|
63
84
|
if (executors.length === 0) {
|
|
64
85
|
throw new Error("fallback requires at least one executor");
|
|
65
86
|
}
|
|
@@ -67,7 +88,8 @@ export function fallback(executors: readonly Executor[], options: FallbackOption
|
|
|
67
88
|
return {
|
|
68
89
|
execute(request, ctx) {
|
|
69
90
|
return runFallback(
|
|
70
|
-
(executor, index) =>
|
|
91
|
+
(executor, index) =>
|
|
92
|
+
executor.execute(request, childContext(ctx, index + 1)),
|
|
71
93
|
executors,
|
|
72
94
|
options,
|
|
73
95
|
);
|
|
@@ -77,11 +99,19 @@ export function fallback(executors: readonly Executor[], options: FallbackOption
|
|
|
77
99
|
let lastError: unknown;
|
|
78
100
|
|
|
79
101
|
for (const [index, executor] of executors.entries()) {
|
|
102
|
+
let emitted = false;
|
|
80
103
|
try {
|
|
81
|
-
|
|
104
|
+
for await (const chunk of executor.stream(
|
|
105
|
+
request,
|
|
106
|
+
childContext(ctx, index + 1),
|
|
107
|
+
)) {
|
|
108
|
+
emitted = true;
|
|
109
|
+
yield chunk;
|
|
110
|
+
}
|
|
82
111
|
return;
|
|
83
112
|
} catch (error) {
|
|
84
113
|
lastError = error;
|
|
114
|
+
if (emitted) throw error;
|
|
85
115
|
const canFallback =
|
|
86
116
|
index < executors.length - 1 &&
|
|
87
117
|
(await shouldContinue(error, index, options.shouldFallback));
|
|
@@ -97,15 +127,21 @@ export function fallback(executors: readonly Executor[], options: FallbackOption
|
|
|
97
127
|
};
|
|
98
128
|
}
|
|
99
129
|
|
|
100
|
-
export function
|
|
101
|
-
const maxIterations = positiveInteger(
|
|
130
|
+
export function runGoal(options: GoalOptions): ExecuteOnly {
|
|
131
|
+
const maxIterations = positiveInteger(
|
|
132
|
+
options.maxIterations ?? 3,
|
|
133
|
+
"goal maxIterations",
|
|
134
|
+
);
|
|
102
135
|
const executeGoal: Executor["execute"] = async (request, ctx) => {
|
|
103
136
|
let current = request;
|
|
104
137
|
let answer = request;
|
|
105
138
|
|
|
106
139
|
for (let attempt = 1; attempt <= maxIterations; attempt += 1) {
|
|
107
140
|
answer = await options.draft.execute(current, childContext(ctx, attempt));
|
|
108
|
-
const review = await options.review.execute(
|
|
141
|
+
const review = await options.review.execute(
|
|
142
|
+
answer,
|
|
143
|
+
childContext(ctx, attempt),
|
|
144
|
+
);
|
|
109
145
|
if (await options.satisfied(review, attempt)) {
|
|
110
146
|
return answer;
|
|
111
147
|
}
|
|
@@ -117,14 +153,7 @@ export function goal(options: GoalOptions): Executor {
|
|
|
117
153
|
return answer;
|
|
118
154
|
};
|
|
119
155
|
|
|
120
|
-
return {
|
|
121
|
-
execute: executeGoal,
|
|
122
|
-
|
|
123
|
-
async *stream(request, ctx) {
|
|
124
|
-
const result = await executeGoal(request, ctx);
|
|
125
|
-
yield result;
|
|
126
|
-
},
|
|
127
|
-
};
|
|
156
|
+
return { execute: executeGoal };
|
|
128
157
|
}
|
|
129
158
|
|
|
130
159
|
async function runRetry<T>(
|
|
@@ -139,7 +168,9 @@ async function runRetry<T>(
|
|
|
139
168
|
return await operation(attempt);
|
|
140
169
|
} catch (error) {
|
|
141
170
|
lastError = error;
|
|
142
|
-
const canRetry =
|
|
171
|
+
const canRetry =
|
|
172
|
+
attempt < attempts &&
|
|
173
|
+
(await shouldContinue(error, attempt, options.shouldRetry));
|
|
143
174
|
if (!canRetry) {
|
|
144
175
|
throw error;
|
|
145
176
|
}
|
package/src/managed/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExecutionTarget, Executor } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function createTargetExecutor(target: ExecutionTarget): Executor {
|
|
4
|
+
return {
|
|
5
|
+
execute(request, context) {
|
|
6
|
+
return context.invoke(request, { target });
|
|
7
|
+
},
|
|
8
|
+
stream(request, context) {
|
|
9
|
+
return context.invokeStream(request, { target });
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -79,7 +79,9 @@ export function appendInternalDraft(
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function createTwoPassExecutor(
|
|
82
|
+
export function createTwoPassExecutor(
|
|
83
|
+
options: TwoPassExecutorOptions,
|
|
84
|
+
): Executor {
|
|
83
85
|
const reasoningIntro = options.reasoningIntro ?? "Let me think...\n\n";
|
|
84
86
|
const reasoningSeparator = options.reasoningSeparator ?? "\n\n";
|
|
85
87
|
|
|
@@ -90,7 +92,11 @@ export function createTwoPassExecutor(options: TwoPassExecutorOptions): Executor
|
|
|
90
92
|
return invokeModel(ctx, draftRequest, options.draftModel);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
const draftResponse = await invokeModel(
|
|
95
|
+
const draftResponse = await invokeModel(
|
|
96
|
+
ctx,
|
|
97
|
+
draftRequest,
|
|
98
|
+
options.draftModel,
|
|
99
|
+
);
|
|
94
100
|
if (callData(draftResponse).length > 0) {
|
|
95
101
|
return draftResponse;
|
|
96
102
|
}
|
|
@@ -124,7 +130,8 @@ export function createTwoPassExecutor(options: TwoPassExecutorOptions): Executor
|
|
|
124
130
|
continue;
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
const toolRequested =
|
|
133
|
+
const toolRequested =
|
|
134
|
+
hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
|
|
128
135
|
if (toolRequested) {
|
|
129
136
|
toolMode = true;
|
|
130
137
|
yield chunk;
|
|
@@ -161,12 +168,17 @@ export function createTwoPassExecutor(options: TwoPassExecutorOptions): Executor
|
|
|
161
168
|
};
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
function buildDraftRequest(
|
|
171
|
+
function buildDraftRequest(
|
|
172
|
+
options: TwoPassExecutorOptions,
|
|
173
|
+
request: Program,
|
|
174
|
+
): Program {
|
|
165
175
|
if (!options.draftSystemPrompt) {
|
|
166
176
|
return request;
|
|
167
177
|
}
|
|
168
178
|
|
|
169
|
-
const systemSpans = messages(request).filter(
|
|
179
|
+
const systemSpans = messages(request).filter(
|
|
180
|
+
(span) => span.role === "system",
|
|
181
|
+
);
|
|
170
182
|
const previousSystemPrompt = systemSpans
|
|
171
183
|
.map((span) => messageText(request, span).trim())
|
|
172
184
|
.filter((text) => text.length > 0)
|
|
@@ -208,7 +220,7 @@ function buildFinalRequest(
|
|
|
208
220
|
|
|
209
221
|
function normalizeDrafts(draft: InternalDraft): string[] {
|
|
210
222
|
const drafts = Array.isArray(draft)
|
|
211
|
-
? draft.map((item) => typeof item === "string" ? item : item.text)
|
|
223
|
+
? draft.map((item) => (typeof item === "string" ? item : item.text))
|
|
212
224
|
: [draft];
|
|
213
225
|
return drafts.map((text) => text.trim()).filter((text) => text.length > 0);
|
|
214
226
|
}
|
package/src/observe.ts
CHANGED
|
@@ -36,7 +36,8 @@ export async function observeExecutionStream(
|
|
|
36
36
|
|
|
37
37
|
for await (const chunk of source) {
|
|
38
38
|
const text = deltaText(chunk);
|
|
39
|
-
const toolRequested =
|
|
39
|
+
const toolRequested =
|
|
40
|
+
hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
|
|
40
41
|
|
|
41
42
|
if (mode === "pending") {
|
|
42
43
|
const clone = cloneProgram(chunk);
|
package/src/output.ts
CHANGED
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
} from "@neutrome/lil-engine";
|
|
9
9
|
import type { OutputSink } from "./types.ts";
|
|
10
10
|
|
|
11
|
-
export async function writeReasoning(
|
|
11
|
+
export async function writeReasoning(
|
|
12
|
+
sink: OutputSink,
|
|
13
|
+
text: string,
|
|
14
|
+
): Promise<void> {
|
|
12
15
|
await sink.write(streamReasoningDelta(text));
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -19,7 +22,10 @@ export function streamTextResponse(text: string): Program[] {
|
|
|
19
22
|
export function streamReasoningDelta(text: string): Program {
|
|
20
23
|
return createProgram({
|
|
21
24
|
code: [
|
|
22
|
-
{
|
|
25
|
+
{
|
|
26
|
+
opcode: Opcode.STREAM_THINK_DELTA,
|
|
27
|
+
value: { kind: "string", value: text },
|
|
28
|
+
},
|
|
23
29
|
],
|
|
24
30
|
});
|
|
25
31
|
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -5,14 +5,19 @@ import {
|
|
|
5
5
|
} from "@neutrome/lil-engine";
|
|
6
6
|
|
|
7
7
|
export function indexOf(program: Program, needle: Instruction): number {
|
|
8
|
-
return program.code.findIndex((instruction) =>
|
|
8
|
+
return program.code.findIndex((instruction) =>
|
|
9
|
+
instructionEquals(instruction, needle),
|
|
10
|
+
);
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function has(program: Program, needle: Instruction): boolean {
|
|
12
14
|
return indexOf(program, needle) >= 0;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export function find(
|
|
17
|
+
export function find(
|
|
18
|
+
program: Program,
|
|
19
|
+
needle: Instruction,
|
|
20
|
+
): Instruction | undefined {
|
|
16
21
|
const i = indexOf(program, needle);
|
|
17
22
|
return i >= 0 ? cloneInstruction(program.code[i]!) : undefined;
|
|
18
23
|
}
|
|
@@ -21,16 +26,22 @@ export function walk(
|
|
|
21
26
|
program: Program,
|
|
22
27
|
callback: (instruction: Instruction, index: number, program: Program) => void,
|
|
23
28
|
): void {
|
|
24
|
-
program.code.forEach((instruction, index) =>
|
|
29
|
+
program.code.forEach((instruction, index) =>
|
|
30
|
+
callback(instruction, index, program),
|
|
31
|
+
);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export function map(
|
|
28
35
|
program: Program,
|
|
29
|
-
callback: (
|
|
36
|
+
callback: (
|
|
37
|
+
instruction: Instruction,
|
|
38
|
+
index: number,
|
|
39
|
+
program: Program,
|
|
40
|
+
) => Instruction,
|
|
30
41
|
): Program {
|
|
31
42
|
return {
|
|
32
43
|
code: program.code.map((instruction, index) =>
|
|
33
|
-
cloneInstruction(callback(instruction, index, program))
|
|
44
|
+
cloneInstruction(callback(instruction, index, program)),
|
|
34
45
|
),
|
|
35
46
|
buffers: cloneBuffers(program.buffers),
|
|
36
47
|
};
|
|
@@ -38,7 +49,11 @@ export function map(
|
|
|
38
49
|
|
|
39
50
|
export function filter(
|
|
40
51
|
program: Program,
|
|
41
|
-
callback: (
|
|
52
|
+
callback: (
|
|
53
|
+
instruction: Instruction,
|
|
54
|
+
index: number,
|
|
55
|
+
program: Program,
|
|
56
|
+
) => boolean,
|
|
42
57
|
): Program {
|
|
43
58
|
return {
|
|
44
59
|
code: program.code
|
|
@@ -50,7 +65,12 @@ export function filter(
|
|
|
50
65
|
|
|
51
66
|
export function reduce<T>(
|
|
52
67
|
program: Program,
|
|
53
|
-
callback: (
|
|
68
|
+
callback: (
|
|
69
|
+
accumulator: T,
|
|
70
|
+
instruction: Instruction,
|
|
71
|
+
index: number,
|
|
72
|
+
program: Program,
|
|
73
|
+
) => T,
|
|
54
74
|
initialValue: T,
|
|
55
75
|
): T {
|
|
56
76
|
let accumulator = initialValue;
|
|
@@ -62,7 +82,11 @@ export function reduce<T>(
|
|
|
62
82
|
|
|
63
83
|
export function all(
|
|
64
84
|
program: Program,
|
|
65
|
-
callback: (
|
|
85
|
+
callback: (
|
|
86
|
+
instruction: Instruction,
|
|
87
|
+
index: number,
|
|
88
|
+
program: Program,
|
|
89
|
+
) => boolean,
|
|
66
90
|
): boolean {
|
|
67
91
|
for (const [index, instruction] of program.code.entries()) {
|
|
68
92
|
if (!callback(instruction, index, program)) return false;
|
|
@@ -72,7 +96,11 @@ export function all(
|
|
|
72
96
|
|
|
73
97
|
export function any(
|
|
74
98
|
program: Program,
|
|
75
|
-
callback: (
|
|
99
|
+
callback: (
|
|
100
|
+
instruction: Instruction,
|
|
101
|
+
index: number,
|
|
102
|
+
program: Program,
|
|
103
|
+
) => boolean,
|
|
76
104
|
): boolean {
|
|
77
105
|
for (const [index, instruction] of program.code.entries()) {
|
|
78
106
|
if (callback(instruction, index, program)) return true;
|
|
@@ -92,9 +120,15 @@ function instructionEquals(left: Instruction, right: Instruction): boolean {
|
|
|
92
120
|
case "float":
|
|
93
121
|
case "int":
|
|
94
122
|
case "ref":
|
|
95
|
-
return
|
|
123
|
+
return (
|
|
124
|
+
right.value.kind === left.value.kind &&
|
|
125
|
+
left.value.value === right.value.value
|
|
126
|
+
);
|
|
96
127
|
case "json":
|
|
97
|
-
return
|
|
128
|
+
return (
|
|
129
|
+
right.value.kind === "json" &&
|
|
130
|
+
bytesEqual(left.value.value, right.value.value)
|
|
131
|
+
);
|
|
98
132
|
case "key_string":
|
|
99
133
|
return (
|
|
100
134
|
right.value.kind === "key_string" &&
|
package/src/stream/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export type { ObservedExecution, StreamObservationHooks } from "../observe.ts";
|
|
2
2
|
|
|
3
3
|
export { observeExecutionStream } from "../observe.ts";
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
streamReasoningDelta,
|
|
6
|
+
streamTextResponse,
|
|
7
|
+
writeReasoning,
|
|
8
|
+
} from "../output.ts";
|
package/src/synthetic/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
addTool,
|
|
2
3
|
appendInstructions,
|
|
3
4
|
createProgram,
|
|
4
|
-
insertAfter,
|
|
5
5
|
insertBefore,
|
|
6
6
|
messages,
|
|
7
7
|
Opcode,
|
|
8
8
|
setModel,
|
|
9
|
-
toolDefinitions,
|
|
10
9
|
type Instruction,
|
|
11
10
|
type Opcode as OpcodeValue,
|
|
12
11
|
type Program,
|
|
@@ -23,11 +22,7 @@ export type ToolInteraction = {
|
|
|
23
22
|
result: unknown;
|
|
24
23
|
};
|
|
25
24
|
|
|
26
|
-
export function
|
|
27
|
-
return setModel(createProgram(), model);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function makeResponse(model: string): Program {
|
|
25
|
+
export function createModelProgram(model: string): Program {
|
|
31
26
|
return setModel(createProgram(), model);
|
|
32
27
|
}
|
|
33
28
|
|
|
@@ -41,14 +36,19 @@ export function prependSystemPrompt(program: Program, text: string): Program {
|
|
|
41
36
|
if (spans.length === 0) {
|
|
42
37
|
return appendInstructions(program, systemMessage);
|
|
43
38
|
}
|
|
44
|
-
|
|
39
|
+
const firstSpan = spans[0];
|
|
40
|
+
if (!firstSpan) return appendInstructions(program, systemMessage);
|
|
41
|
+
return insertBefore(program, firstSpan.start, systemMessage);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
export function appendUserMessage(program: Program, text: string): Program {
|
|
48
45
|
return appendInstructions(program, buildMessage("user", text));
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
export function appendAssistantMessage(
|
|
48
|
+
export function appendAssistantMessage(
|
|
49
|
+
program: Program,
|
|
50
|
+
text: string,
|
|
51
|
+
): Program {
|
|
52
52
|
return appendInstructions(program, buildMessage("assistant", text));
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -63,7 +63,10 @@ export function makeToolCall(
|
|
|
63
63
|
{ opcode: Opcode.ROLE_AST, value: { kind: "none" } },
|
|
64
64
|
{ opcode: Opcode.CALL_START, value: { kind: "string", value: callId } },
|
|
65
65
|
{ opcode: Opcode.CALL_NAME, value: { kind: "string", value: name } },
|
|
66
|
-
{
|
|
66
|
+
{
|
|
67
|
+
opcode: Opcode.CALL_ARGS,
|
|
68
|
+
value: { kind: "json", value: encodeJson(args) },
|
|
69
|
+
},
|
|
67
70
|
{ opcode: Opcode.CALL_END, value: { kind: "none" } },
|
|
68
71
|
{ opcode: Opcode.MSG_END, value: { kind: "none" } },
|
|
69
72
|
],
|
|
@@ -79,38 +82,15 @@ export function appendToolInteraction(
|
|
|
79
82
|
interaction: ToolInteraction,
|
|
80
83
|
): Program {
|
|
81
84
|
return appendInstructions(program, [
|
|
82
|
-
...makeToolCall(
|
|
85
|
+
...makeToolCall(
|
|
86
|
+
interaction.name,
|
|
87
|
+
interaction.args ?? {},
|
|
88
|
+
interaction.callId,
|
|
89
|
+
).code,
|
|
83
90
|
...buildToolResponse(interaction.callId, interaction.result),
|
|
84
91
|
]);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
export function addTool(
|
|
88
|
-
program: Program,
|
|
89
|
-
name: string,
|
|
90
|
-
description: string,
|
|
91
|
-
schemaJson: Uint8Array,
|
|
92
|
-
): Program {
|
|
93
|
-
const definition: Instruction[] = [
|
|
94
|
-
{ opcode: Opcode.DEF_START, value: { kind: "none" } },
|
|
95
|
-
{ opcode: Opcode.DEF_NAME, value: { kind: "string", value: name } },
|
|
96
|
-
{ opcode: Opcode.DEF_DESC, value: { kind: "string", value: description } },
|
|
97
|
-
{ opcode: Opcode.DEF_SCHEMA, value: { kind: "json", value: schemaJson.slice() } },
|
|
98
|
-
{ opcode: Opcode.DEF_END, value: { kind: "none" } },
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
const defs = toolDefinitions(program);
|
|
102
|
-
if (defs.length > 0) {
|
|
103
|
-
return insertAfter(program, defs.at(-1)!.end, definition);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const spans = messages(program);
|
|
107
|
-
if (spans.length > 0) {
|
|
108
|
-
return insertBefore(program, spans[0]!.start, definition);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return appendInstructions(program, definition);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
94
|
function buildMessage(role: TextMessageRole, text: string): Instruction[] {
|
|
115
95
|
return [
|
|
116
96
|
{ opcode: Opcode.MSG_START, value: { kind: "none" } },
|
|
@@ -125,7 +105,10 @@ function buildToolResponse(callId: string, result: unknown): Instruction[] {
|
|
|
125
105
|
{ opcode: Opcode.MSG_START, value: { kind: "none" } },
|
|
126
106
|
{ opcode: Opcode.ROLE_TOOL, value: { kind: "none" } },
|
|
127
107
|
{ opcode: Opcode.RESULT_START, value: { kind: "string", value: callId } },
|
|
128
|
-
{
|
|
108
|
+
{
|
|
109
|
+
opcode: Opcode.RESULT_DATA,
|
|
110
|
+
value: { kind: "string", value: stringifyResult(result) },
|
|
111
|
+
},
|
|
129
112
|
{ opcode: Opcode.RESULT_END, value: { kind: "none" } },
|
|
130
113
|
{ opcode: Opcode.MSG_END, value: { kind: "none" } },
|
|
131
114
|
];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { addTool, type Program } from "@neutrome/lil-engine";
|
|
2
|
+
import { prependSystemPrompt } from "./synthetic/index.ts";
|
|
3
|
+
import {
|
|
4
|
+
createExecutionEvent,
|
|
5
|
+
type ExecutorContext,
|
|
6
|
+
type Tool,
|
|
7
|
+
} from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export type ToolCall = { id: string; name: string; args: string };
|
|
10
|
+
export type ToolExecution = ToolCall & { result: string };
|
|
11
|
+
|
|
12
|
+
export class ToolArgumentsError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
readonly toolName: string,
|
|
15
|
+
readonly argumentsText: string,
|
|
16
|
+
) {
|
|
17
|
+
super(`Tool "${toolName}" received invalid JSON object arguments`);
|
|
18
|
+
this.name = "ToolArgumentsError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildToolAugmenter(tools: readonly Tool[]) {
|
|
23
|
+
const encoder = new TextEncoder();
|
|
24
|
+
const fragments = tools
|
|
25
|
+
.map((tool) => tool.systemPromptFragment)
|
|
26
|
+
.filter((fragment): fragment is string => Boolean(fragment));
|
|
27
|
+
|
|
28
|
+
return (request: Program): Program => {
|
|
29
|
+
let augmented = request;
|
|
30
|
+
if (fragments.length > 0) {
|
|
31
|
+
augmented = prependSystemPrompt(augmented, fragments.join("\n\n"));
|
|
32
|
+
}
|
|
33
|
+
for (const tool of tools) {
|
|
34
|
+
augmented = addTool(
|
|
35
|
+
augmented,
|
|
36
|
+
tool.name,
|
|
37
|
+
tool.description,
|
|
38
|
+
encoder.encode(JSON.stringify(tool.schema)),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return augmented;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildCallExecutor(toolMap: ReadonlyMap<string, Tool>) {
|
|
46
|
+
return async (
|
|
47
|
+
calls: readonly ToolCall[],
|
|
48
|
+
ctx: ExecutorContext,
|
|
49
|
+
): Promise<ToolExecution[]> =>
|
|
50
|
+
Promise.all(
|
|
51
|
+
calls.map(async (call) => {
|
|
52
|
+
const tool = toolMap.get(call.name);
|
|
53
|
+
if (!tool) {
|
|
54
|
+
throw new Error(`No tool is registered for call "${call.name}"`);
|
|
55
|
+
}
|
|
56
|
+
const startedMs = Date.now();
|
|
57
|
+
const result = await tool.execute(
|
|
58
|
+
parseToolArguments(call.name, call.args),
|
|
59
|
+
ctx,
|
|
60
|
+
);
|
|
61
|
+
const finishedMs = Date.now();
|
|
62
|
+
ctx.observe(
|
|
63
|
+
createExecutionEvent({
|
|
64
|
+
kind: "tool.executed",
|
|
65
|
+
requestId: ctx.requestId,
|
|
66
|
+
executionId: `tool_${call.id || crypto.randomUUID()}`,
|
|
67
|
+
parentExecutionId: ctx.executionId,
|
|
68
|
+
data: {
|
|
69
|
+
toolName: call.name,
|
|
70
|
+
toolCallId: call.id,
|
|
71
|
+
startedAt: new Date(startedMs).toISOString(),
|
|
72
|
+
finishedAt: new Date(finishedMs).toISOString(),
|
|
73
|
+
durationMs: finishedMs - startedMs,
|
|
74
|
+
lilText: toolTrace(call, result),
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
return { ...call, result };
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseToolArguments(
|
|
84
|
+
toolName: string,
|
|
85
|
+
argumentsText: string,
|
|
86
|
+
): Record<string, unknown> {
|
|
87
|
+
try {
|
|
88
|
+
const value: unknown = JSON.parse(argumentsText);
|
|
89
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
90
|
+
throw new ToolArgumentsError(toolName, argumentsText);
|
|
91
|
+
}
|
|
92
|
+
return value as Record<string, unknown>;
|
|
93
|
+
} catch {
|
|
94
|
+
throw new ToolArgumentsError(toolName, argumentsText);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toolTrace(call: ToolCall, result: string): string {
|
|
99
|
+
return [
|
|
100
|
+
`CALL_START ${JSON.stringify(call.id)}`,
|
|
101
|
+
`CALL_NAME ${JSON.stringify(call.name)}`,
|
|
102
|
+
`CALL_ARGS ${call.args}`,
|
|
103
|
+
"CALL_END",
|
|
104
|
+
"RESULT_START",
|
|
105
|
+
`RESULT_DATA ${JSON.stringify(result)}`,
|
|
106
|
+
"RESULT_END",
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|