@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 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 { observeExecutionStream, writeReasoning } from "@neutrome/lilsdk-ts/stream";
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/tool synthesis helpers such as `makeRequest`, `makeResponse`, `makeMessage`, `makeToolCall`, `makeToolResponse`, and `appendToolInteraction`
17
- - `@neutrome/lilsdk-ts/tools`: `connectTools` and `withTools` tool-call loops
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 `goal` executor control-flow helpers
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.2.5",
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.2.1"
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
- SdkEvent,
9
- SdkEventInput,
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";
@@ -8,7 +8,10 @@ export type RetryOptions = {
8
8
  };
9
9
 
10
10
  export type FallbackOptions = {
11
- shouldFallback?: (error: unknown, executorIndex: number) => boolean | Promise<boolean>;
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 function retry(executor: Executor, options: RetryOptions = {}): Executor {
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
- yield* executor.stream(request, childContext(ctx, attempt));
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
- const canRetry = attempt < attempts && (await shouldContinue(error, attempt, options.shouldRetry));
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(executors: readonly Executor[], options: FallbackOptions = {}): Executor {
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) => executor.execute(request, childContext(ctx, index + 1)),
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
- yield* executor.stream(request, childContext(ctx, index + 1));
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 goal(options: GoalOptions): Executor {
101
- const maxIterations = positiveInteger(options.maxIterations ?? 3, "goal maxIterations");
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(answer, childContext(ctx, attempt));
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 = attempt < attempts && (await shouldContinue(error, attempt, options.shouldRetry));
171
+ const canRetry =
172
+ attempt < attempts &&
173
+ (await shouldContinue(error, attempt, options.shouldRetry));
143
174
  if (!canRetry) {
144
175
  throw error;
145
176
  }
@@ -1,4 +1,4 @@
1
- export { wrap, wrapNoAcl } from "./wrap.ts";
1
+ export { createTargetExecutor } from "./target-executor.ts";
2
2
 
3
3
  export {
4
4
  appendInternalDraft,
@@ -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(options: TwoPassExecutorOptions): Executor {
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(ctx, draftRequest, options.draftModel);
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 = hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
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(options: TwoPassExecutorOptions, request: Program): Program {
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((span) => span.role === "system");
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 = hasToolDelta(chunk) || finishReason(chunk) === "tool_calls";
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(sink: OutputSink, text: string): Promise<void> {
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
- { opcode: Opcode.STREAM_THINK_DELTA, value: { kind: "string", value: text } },
25
+ {
26
+ opcode: Opcode.STREAM_THINK_DELTA,
27
+ value: { kind: "string", value: text },
28
+ },
23
29
  ],
24
30
  });
25
31
  }
@@ -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) => instructionEquals(instruction, needle));
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(program: Program, needle: Instruction): Instruction | undefined {
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) => callback(instruction, index, program));
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: (instruction: Instruction, index: number, program: Program) => Instruction,
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: (instruction: Instruction, index: number, program: Program) => boolean,
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: (accumulator: T, instruction: Instruction, index: number, program: Program) => T,
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: (instruction: Instruction, index: number, program: Program) => boolean,
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: (instruction: Instruction, index: number, program: Program) => boolean,
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 right.value.kind === left.value.kind && left.value.value === right.value.value;
123
+ return (
124
+ right.value.kind === left.value.kind &&
125
+ left.value.value === right.value.value
126
+ );
96
127
  case "json":
97
- return right.value.kind === "json" && bytesEqual(left.value.value, right.value.value);
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" &&
@@ -1,4 +1,8 @@
1
1
  export type { ObservedExecution, StreamObservationHooks } from "../observe.ts";
2
2
 
3
3
  export { observeExecutionStream } from "../observe.ts";
4
- export { streamReasoningDelta, streamTextResponse, writeReasoning } from "../output.ts";
4
+ export {
5
+ streamReasoningDelta,
6
+ streamTextResponse,
7
+ writeReasoning,
8
+ } from "../output.ts";
@@ -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 makeRequest(model: string): Program {
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
- return insertBefore(program, spans[0]!.start, systemMessage);
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(program: Program, text: string): Program {
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
- { opcode: Opcode.CALL_ARGS, value: { kind: "json", value: encodeJson(args) } },
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(interaction.name, interaction.args ?? {}, interaction.callId).code,
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
- { opcode: Opcode.RESULT_DATA, value: { kind: "string", value: stringifyResult(result) } },
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
+ }