@neutrome/lilsdk 0.4.6 → 0.5.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/src/output.ts CHANGED
@@ -1,31 +1,19 @@
1
- import {
2
- createProgram,
3
- Opcode,
4
- streamEnd,
5
- streamStart,
6
- streamTextDelta,
7
- type Program,
8
- } from "@neutrome/lil-engine";
1
+ import { delta } from "@neutrome/lil-engine";
9
2
  import type { OutputSink } from "./types.ts";
10
3
 
11
- export async function writeReasoning(
4
+ /**
5
+ * Writes a model-reasoning chunk to a sink.
6
+ *
7
+ * @param sink - Sink to write to.
8
+ * @param text - Reasoning text to emit.
9
+ * @example
10
+ * ```ts
11
+ * await writeThinking(sink, "Checking the attachment first.");
12
+ * ```
13
+ */
14
+ export async function writeThinking(
12
15
  sink: OutputSink,
13
16
  text: string,
14
17
  ): 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
- });
18
+ await sink.write(delta.thinking(text));
31
19
  }
@@ -4,16 +4,37 @@ import {
4
4
  type Program,
5
5
  } from "@neutrome/lil-engine";
6
6
 
7
+ /**
8
+ * Finds the position of an instruction by value.
9
+ *
10
+ * @param program - Program to search.
11
+ * @param needle - Instruction to match, compared by opcode and payload.
12
+ * @returns The index, or `-1` when absent.
13
+ */
7
14
  export function indexOf(program: Program, needle: Instruction): number {
8
15
  return program.code.findIndex((instruction) =>
9
16
  instructionEquals(instruction, needle),
10
17
  );
11
18
  }
12
19
 
20
+ /**
21
+ * Reports whether a program contains an instruction by value.
22
+ *
23
+ * @param program - Program to search.
24
+ * @param needle - Instruction to match.
25
+ * @returns `true` when a matching instruction exists.
26
+ */
13
27
  export function has(program: Program, needle: Instruction): boolean {
14
28
  return indexOf(program, needle) >= 0;
15
29
  }
16
30
 
31
+ /**
32
+ * Reads the first instruction matching a value.
33
+ *
34
+ * @param program - Program to search.
35
+ * @param needle - Instruction to match.
36
+ * @returns A copy of the match, or `undefined` when absent.
37
+ */
17
38
  export function find(
18
39
  program: Program,
19
40
  needle: Instruction,
@@ -22,6 +43,12 @@ export function find(
22
43
  return i >= 0 ? cloneInstruction(program.code[i]!) : undefined;
23
44
  }
24
45
 
46
+ /**
47
+ * Visits every instruction in order.
48
+ *
49
+ * @param program - Program to walk.
50
+ * @param callback - Called with each instruction, its index and the program.
51
+ */
25
52
  export function walk(
26
53
  program: Program,
27
54
  callback: (instruction: Instruction, index: number, program: Program) => void,
@@ -31,6 +58,19 @@ export function walk(
31
58
  );
32
59
  }
33
60
 
61
+ /**
62
+ * Rewrites every instruction.
63
+ *
64
+ * @param program - Program to map.
65
+ * @param callback - Returns the replacement for each instruction.
66
+ * @returns A new program; the input is left untouched.
67
+ * @example
68
+ * ```ts
69
+ * const redacted = map(program, (instruction) =>
70
+ * instruction.opcode === Opcode.TXT_CHUNK ? redact(instruction) : instruction,
71
+ * );
72
+ * ```
73
+ */
34
74
  export function map(
35
75
  program: Program,
36
76
  callback: (
@@ -47,6 +87,13 @@ export function map(
47
87
  };
48
88
  }
49
89
 
90
+ /**
91
+ * Keeps only the instructions a predicate accepts.
92
+ *
93
+ * @param program - Program to filter.
94
+ * @param callback - Returns `true` for instructions to keep.
95
+ * @returns A new program; the input is left untouched.
96
+ */
50
97
  export function filter(
51
98
  program: Program,
52
99
  callback: (
@@ -63,6 +110,14 @@ export function filter(
63
110
  };
64
111
  }
65
112
 
113
+ /**
114
+ * Folds the instruction list into a single value.
115
+ *
116
+ * @param program - Program to fold.
117
+ * @param callback - Combines the accumulator with each instruction.
118
+ * @param initialValue - Starting accumulator.
119
+ * @returns The final accumulator.
120
+ */
66
121
  export function reduce<T>(
67
122
  program: Program,
68
123
  callback: (
@@ -80,6 +135,13 @@ export function reduce<T>(
80
135
  return accumulator;
81
136
  }
82
137
 
138
+ /**
139
+ * Reports whether every instruction satisfies a predicate.
140
+ *
141
+ * @param program - Program to test.
142
+ * @param callback - Predicate applied to each instruction.
143
+ * @returns `true` when all instructions pass, including for empty programs.
144
+ */
83
145
  export function all(
84
146
  program: Program,
85
147
  callback: (
@@ -94,6 +156,13 @@ export function all(
94
156
  return true;
95
157
  }
96
158
 
159
+ /**
160
+ * Reports whether any instruction satisfies a predicate.
161
+ *
162
+ * @param program - Program to test.
163
+ * @param callback - Predicate applied to each instruction.
164
+ * @returns `true` when at least one instruction passes.
165
+ */
97
166
  export function any(
98
167
  program: Program,
99
168
  callback: (
@@ -1,9 +1,5 @@
1
1
  export type { ObservedExecution, StreamObservationHooks } from "../observe.ts";
2
2
 
3
3
  export { observeExecutionStream } from "../observe.ts";
4
- export { completeStream, streamStage } from "./stages.ts";
5
- export {
6
- streamReasoningDelta,
7
- streamTextResponse,
8
- writeReasoning,
9
- } from "../output.ts";
4
+ export { streamStage } from "./stages.ts";
5
+ export { writeThinking } from "../output.ts";
@@ -1,6 +1,19 @@
1
- import { createProgram, Opcode, type Program } from "@neutrome/lil-engine";
1
+ import { Opcode, type Program } from "@neutrome/lil-engine";
2
2
 
3
- /** Streams one stage without allowing it to complete the enclosing stream. */
3
+ /**
4
+ * Forwards one stage of a stream without letting it close the outer stream.
5
+ *
6
+ * Drops the `RESP_DONE` and `STREAM_END` instructions, so several executors can
7
+ * stream into one response and only the last one finishes it.
8
+ *
9
+ * @param source - Chunks to forward.
10
+ * @returns The chunks, minus any terminal instructions.
11
+ * @example
12
+ * ```ts
13
+ * for await (const chunk of streamStage(ctx.invokeStream(draft, request))) yield chunk;
14
+ * yield delta.end();
15
+ * ```
16
+ */
4
17
  export async function* streamStage(
5
18
  source: AsyncIterable<Program>,
6
19
  ): AsyncGenerator<Program> {
@@ -14,12 +27,3 @@ export async function* streamStage(
14
27
  yield code.length === chunk.code.length ? chunk : { ...chunk, code };
15
28
  }
16
29
  }
17
-
18
- export function completeStream(reason = "stop"): Program {
19
- return createProgram({
20
- code: [
21
- { opcode: Opcode.RESP_DONE, value: { kind: "string", value: reason } },
22
- { opcode: Opcode.STREAM_END, value: { kind: "none" } },
23
- ],
24
- });
25
- }
@@ -1,5 +1,8 @@
1
- import { addTool, type Program } from "@neutrome/lil-engine";
2
- import { prependSystemPrompt } from "./synthetic/index.ts";
1
+ import {
2
+ appendTool,
3
+ prependSystemPrompt,
4
+ type Program,
5
+ } from "@neutrome/lil-engine";
3
6
  import {
4
7
  createExecutionEvent,
5
8
  type ExecutorContext,
@@ -14,6 +17,9 @@ export type ToolExecution = {
14
17
  result: string;
15
18
  };
16
19
 
20
+ /**
21
+ * Thrown when a model calls a tool with arguments that are not valid JSON.
22
+ */
17
23
  export class ToolArgumentsError extends Error {
18
24
  constructor(
19
25
  readonly toolName: string,
@@ -36,7 +42,7 @@ export function buildToolAugmenter(tools: readonly Tool[]) {
36
42
  augmented = prependSystemPrompt(augmented, fragments.join("\n\n"));
37
43
  }
38
44
  for (const tool of tools) {
39
- augmented = addTool(
45
+ augmented = appendTool(
40
46
  augmented,
41
47
  tool.name,
42
48
  tool.description,
package/src/tools.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import {
2
- callData,
2
+ appendToolInteraction,
3
3
  createProgram,
4
- decodeStreamToolDelta,
4
+ delta,
5
+ extractToolCalls,
5
6
  isStreaming,
6
7
  Opcode,
7
8
  setStreaming,
8
9
  type Instruction,
9
10
  type Program,
10
- type StreamToolDelta,
11
11
  } from "@neutrome/lil-engine";
12
- import { appendToolInteraction } from "./synthetic/index.ts";
13
12
  import {
14
13
  type Executor,
15
14
  type ExecutorContext,
@@ -26,14 +25,16 @@ import {
26
25
 
27
26
  export { ToolArgumentsError } from "./tools-support.ts";
28
27
 
29
- export type WithToolsOptions = {
28
+ /** Options accepted by {@link createToolsExecutor}. */
29
+ export type ToolsExecutorOptions = {
30
+ /** Maximum tool round trips before the loop gives up. Defaults to 10. */
30
31
  maxIterations?: number;
31
32
  };
32
33
 
33
34
  const DEFAULT_MAX_ITERATIONS = 10;
34
35
 
35
36
  type ToolDeltaAcc = { index: number; id: string; name: string; args: string };
36
- type DecodedToolDelta = { instruction: Instruction; delta: StreamToolDelta };
37
+ type DecodedToolDelta = { instruction: Instruction; delta: delta.ToolCall };
37
38
 
38
39
  function collectToolDeltasFromChunk(
39
40
  chunk: Program,
@@ -41,19 +42,19 @@ function collectToolDeltasFromChunk(
41
42
  ): DecodedToolDelta[] {
42
43
  const deltas: DecodedToolDelta[] = [];
43
44
  for (const instr of chunk.code) {
44
- const delta = decodeStreamToolDelta(instr);
45
- if (!delta) continue;
46
- deltas.push({ instruction: instr, delta });
47
- const current = acc.get(delta.index) ?? {
48
- index: delta.index,
45
+ const toolCall = delta.decodeToolCall(instr);
46
+ if (!toolCall) continue;
47
+ deltas.push({ instruction: instr, delta: toolCall });
48
+ const current = acc.get(toolCall.index) ?? {
49
+ index: toolCall.index,
49
50
  id: "",
50
51
  name: "",
51
52
  args: "",
52
53
  };
53
- if (delta.id) current.id = delta.id;
54
- if (delta.name) current.name = delta.name;
55
- if (delta.arguments) current.args += delta.arguments;
56
- acc.set(delta.index, current);
54
+ if (toolCall.id) current.id = toolCall.id;
55
+ if (toolCall.name) current.name = toolCall.name;
56
+ if (toolCall.arguments) current.args += toolCall.arguments;
57
+ acc.set(toolCall.index, current);
57
58
  }
58
59
  return deltas;
59
60
  }
@@ -179,10 +180,26 @@ async function* streamToolLoop(
179
180
  });
180
181
  }
181
182
 
182
- export function connectTools(
183
- tools: Tool[],
183
+ /**
184
+ * Wraps an executor in a tool loop.
185
+ *
186
+ * The returned executor declares `tools` to the model, runs any it owns, feeds
187
+ * the results back and repeats. Calls to tools it does not own are passed
188
+ * through to the caller untouched, so an outer loop can handle them.
189
+ *
190
+ * @param inner - Executor or model id that does the generating.
191
+ * @param tools - Tools this loop owns and executes.
192
+ * @param options - Loop limits.
193
+ * @returns An executor that resolves tool calls before answering.
194
+ * @example
195
+ * ```ts
196
+ * export default createToolsExecutor("core/turn-1", [searchTool, calculatorTool]);
197
+ * ```
198
+ */
199
+ export function createToolsExecutor(
184
200
  inner: ExecutorInput,
185
- options: WithToolsOptions = {},
201
+ tools: Tool[],
202
+ options: ToolsExecutorOptions = {},
186
203
  ): Executor {
187
204
  const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
188
205
  const toolMap = new Map(tools.map((t) => [t.name, t]));
@@ -198,7 +215,7 @@ export function connectTools(
198
215
 
199
216
  while (canRunToolLoop(state, maxIterations)) {
200
217
  const response = await ctx.invoke(inner, state.request);
201
- const calls = callData(response);
218
+ const calls = extractToolCalls(response);
202
219
  const decision = decideToolLoop(calls, toolMap);
203
220
  if (decision.kind !== "continue") return response;
204
221
 
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Program } from "@neutrome/lil-engine";
2
2
 
3
+ /** A capability a {@link ProgramTransform} declares before it may be applied. */
3
4
  export type TransformCapability =
4
5
  | "read_messages"
5
6
  | "write_messages"
@@ -8,77 +9,201 @@ export type TransformCapability =
8
9
  | "drop_content"
9
10
  | "provider_extension";
10
11
 
12
+ /** One audit record emitted while a request is being served. */
11
13
  export type ExecutionEvent = {
14
+ /** Event name, such as `"executor.start"`. */
12
15
  kind: string;
16
+ /** Id shared by every event of one inbound request. */
13
17
  requestId: string;
18
+ /** Id of the execution that emitted the event. */
14
19
  executionId: string;
20
+ /** ISO timestamp of the event. */
15
21
  timestamp: string;
22
+ /** Id of the enclosing execution, when nested. */
16
23
  parentExecutionId?: string;
24
+ /** What the execution was talking to. */
17
25
  target?: { kind: "provider" | "executor"; id: string };
26
+ /** Error class, for failure events. */
18
27
  errorKind?: string;
28
+ /** Free-form payload. */
19
29
  data?: Record<string, unknown>;
20
30
  };
21
31
 
32
+ /** {@link ExecutionEvent} before a timestamp is stamped on it. */
22
33
  export type ExecutionEventInput = Omit<ExecutionEvent, "timestamp"> & {
23
34
  timestamp?: string;
24
35
  };
25
36
 
37
+ /** Expiry options accepted by {@link Cache.put}. */
26
38
  export type CachePutOptions = {
39
+ /** Absolute expiry, as a Unix timestamp in seconds. */
27
40
  expiration?: number;
41
+ /** Relative expiry, in seconds from now. */
28
42
  expirationTtl?: number;
29
43
  };
30
44
 
45
+ /** Key-value store handed to executors through {@link RuntimeContext.cache}. */
31
46
  export type Cache = {
47
+ /** Reads a value, or `null` when the key is unset. */
32
48
  get(key: string): Promise<string | null>;
49
+ /** Writes a value, optionally with an expiry. */
33
50
  put(key: string, value: string, options?: CachePutOptions): Promise<void>;
51
+ /** Removes a key. */
34
52
  delete(key: string): Promise<void>;
35
53
  };
36
54
 
37
- type RuntimeContext = {
55
+ /**
56
+ * The runtime services available while a request is being served.
57
+ *
58
+ * Executors and transforms never talk to providers directly: they call
59
+ * {@link RuntimeContext.invoke}, which lets the runtime handle routing,
60
+ * auditing and cancellation.
61
+ */
62
+ export type RuntimeContext = {
63
+ /** Request-scoped cache. */
38
64
  cache: Cache;
65
+ /**
66
+ * Runs another executor, or a model id, to completion.
67
+ *
68
+ * @param executor - Executor value or model id to run.
69
+ * @param request - Program to send.
70
+ * @returns The response program.
71
+ */
39
72
  invoke(executor: ExecutorInput, request: Program): Promise<Program>;
73
+ /**
74
+ * Runs another executor, or a model id, and streams its chunks.
75
+ *
76
+ * @param executor - Executor value or model id to run.
77
+ * @param request - Program to send.
78
+ * @returns The response chunks.
79
+ */
40
80
  invokeStream(
41
81
  executor: ExecutorInput,
42
82
  request: Program,
43
83
  ): AsyncIterable<Program>;
84
+ /**
85
+ * Records an audit event.
86
+ *
87
+ * @param event - Event to record.
88
+ */
44
89
  observe(event: ExecutionEvent): void;
90
+ /** Aborts when the client goes away or the request times out. */
45
91
  signal: AbortSignal;
46
92
  };
47
93
 
48
- export type TransformContext = RuntimeContext;
49
-
94
+ /** A named, capability-scoped edit applied to every program passing through. */
50
95
  export type ProgramTransform = {
96
+ /** Transform name, used in audit events. */
51
97
  name: string;
98
+ /** Everything this transform is allowed to do. */
52
99
  capabilities: TransformCapability[];
53
- apply(program: Program, ctx: TransformContext): Program | Promise<Program>;
100
+ /**
101
+ * Applies the transform.
102
+ *
103
+ * @param program - Program to edit.
104
+ * @param ctx - Runtime services.
105
+ * @returns The edited program.
106
+ */
107
+ apply(program: Program, ctx: RuntimeContext): Program | Promise<Program>;
54
108
  };
55
109
 
110
+ /** {@link RuntimeContext} plus the identity of the running execution. */
56
111
  export type ExecutorContext = RuntimeContext & {
112
+ /** Id shared by every execution of one inbound request. */
57
113
  requestId: string;
114
+ /** Id of this execution. */
58
115
  executionId: string;
116
+ /** Id of the enclosing execution, when nested. */
59
117
  parentExecutionId?: string;
60
118
  };
61
119
 
120
+ /**
121
+ * The unit of composition: something that answers a program.
122
+ *
123
+ * Both methods must be implemented, so an executor can be used in a streaming
124
+ * and a non-streaming request alike.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * const executor: Executor = {
129
+ * async execute(request, ctx) {
130
+ * return ctx.invoke(upstream, prependSystemPrompt(request, system()));
131
+ * },
132
+ * async *stream(request, ctx) {
133
+ * yield* ctx.invokeStream(upstream, prependSystemPrompt(request, system()));
134
+ * },
135
+ * };
136
+ * ```
137
+ */
62
138
  export type Executor = {
139
+ /**
140
+ * Answers a request in one shot.
141
+ *
142
+ * @param request - Program to answer.
143
+ * @param ctx - Runtime services and execution identity.
144
+ * @returns The response program.
145
+ */
63
146
  execute(request: Program, ctx: ExecutorContext): Promise<Program>;
147
+ /**
148
+ * Answers a request as a chunk stream.
149
+ *
150
+ * @param request - Program to answer.
151
+ * @param ctx - Runtime services and execution identity.
152
+ * @returns The response chunks.
153
+ */
64
154
  stream(request: Program, ctx: ExecutorContext): AsyncIterable<Program>;
65
155
  };
66
156
 
157
+ /**
158
+ * An executor, or the id of a model to run.
159
+ *
160
+ * Every combinator in this package accepts either, so `retry("fast/gemma-4-31b")`
161
+ * and `retry(myExecutor)` are both valid.
162
+ */
67
163
  export type ExecutorInput = string | Executor;
68
164
 
165
+ /** A tool the model may call during a tool loop. */
69
166
  export type Tool = {
167
+ /** Tool name the model must use when calling it. */
70
168
  name: string;
169
+ /** Natural-language description shown to the model. */
71
170
  description: string;
171
+ /** JSON Schema describing the arguments. */
72
172
  schema: Record<string, unknown>;
173
+ /** Extra instructions appended to the system prompt when this tool is enabled. */
73
174
  systemPromptFragment?: string;
175
+ /**
176
+ * Runs the tool.
177
+ *
178
+ * @param args - Arguments the model supplied, already parsed.
179
+ * @param ctx - Runtime services and execution identity.
180
+ * @returns The result, rendered as text for the model.
181
+ */
74
182
  execute(args: Record<string, unknown>, ctx: ExecutorContext): Promise<string>;
75
183
  };
76
184
 
185
+ /** Where streamed chunks are written. */
77
186
  export type OutputSink = {
187
+ /** Writes one chunk. */
78
188
  write(chunk: Program): void | Promise<void>;
189
+ /** Closes the stream. */
79
190
  close(): void | Promise<void>;
80
191
  };
81
192
 
193
+ /**
194
+ * Stamps an audit event with the current time.
195
+ *
196
+ * @param input - Event details, without a timestamp.
197
+ * @returns A complete event, ready to pass to {@link RuntimeContext.observe}.
198
+ * @example
199
+ * ```ts
200
+ * ctx.observe(createExecutionEvent({
201
+ * kind: "executor.start",
202
+ * requestId: ctx.requestId,
203
+ * executionId: ctx.executionId,
204
+ * }));
205
+ * ```
206
+ */
82
207
  export function createExecutionEvent(
83
208
  input: ExecutionEventInput,
84
209
  ): ExecutionEvent {