@msm-core/mini 0.5.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/memory-store.d.ts +38 -0
- package/dist/adapters/memory-store.js +73 -0
- package/dist/adapters/redis-memory.d.ts +13 -8
- package/dist/adapters/redis-memory.js +5 -0
- package/dist/brain/anthropic.js +50 -17
- package/dist/brain/gemini.js +50 -17
- package/dist/brain/ollama.js +68 -19
- package/dist/brain/openai.js +57 -23
- package/dist/brain/streaming.d.ts +315 -0
- package/dist/brain/streaming.js +439 -0
- package/dist/brain/tool-context.d.ts +50 -2
- package/dist/brain/tool-context.js +88 -0
- package/dist/bridge/pipeline.js +11 -8
- package/dist/core/context-builder.d.ts +11 -0
- package/dist/core/context-builder.js +11 -1
- package/dist/core/hooks.d.ts +10 -0
- package/dist/core/hooks.js +14 -0
- package/dist/core/loop.d.ts +43 -1
- package/dist/core/loop.js +749 -98
- package/dist/core/types.d.ts +261 -1
- package/dist/core/types.js +40 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -0
- package/dist/tools/delegate.d.ts +134 -0
- package/dist/tools/delegate.js +223 -0
- package/package.json +11 -11
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming — the four providers' wire formats reduced to pure functions (ب١).
|
|
3
|
+
*
|
|
4
|
+
* **Why this file exists at all.** A streamed answer and a non-streamed answer
|
|
5
|
+
* are the same answer delivered twice over. The only way to be sure of that is
|
|
6
|
+
* to be able to *prove* it, and you cannot prove anything about code that only
|
|
7
|
+
* runs with a socket open and a key in the environment. So every provider's
|
|
8
|
+
* stream is split in two here:
|
|
9
|
+
*
|
|
10
|
+
* - `…Delta(event)` — what text just arrived. Pure.
|
|
11
|
+
* - `accumulate…(events)` — the events folded back into the SAME object shape
|
|
12
|
+
* the provider's NON-streaming call returns. Pure.
|
|
13
|
+
*
|
|
14
|
+
* Feed `accumulate…` a fixed array of chunks in a test and you get an object
|
|
15
|
+
* that the brain's own post-call code — not a copy of it, the same lines —
|
|
16
|
+
* turns into a payload. That is what makes "the stream aggregates to what the
|
|
17
|
+
* non-stream returns" a checkable claim rather than a hope, and it is why the
|
|
18
|
+
* brains route BOTH paths into one `response` variable and one payload builder.
|
|
19
|
+
*
|
|
20
|
+
* **The read set is the contract.** Each `…Response` interface below is exactly
|
|
21
|
+
* the fields its brain reads off a provider response — no more. It is small on
|
|
22
|
+
* purpose: `id`, `created`, `finish_reason`, block ids and the rest are things
|
|
23
|
+
* a stream cannot reconstruct and no brain here consumes, so promising them
|
|
24
|
+
* would be a lie the compiler would happily keep.
|
|
25
|
+
*
|
|
26
|
+
* **Display, not truth (the governing limit of ب١).** Nothing in this file
|
|
27
|
+
* writes a log event, touches a fingerprint, or decides anything. Chunks are
|
|
28
|
+
* emitted for a human to look at; the payload the brain returns is the record.
|
|
29
|
+
*
|
|
30
|
+
* **Text only in v1.** Tool-call fragments are accumulated but never emitted —
|
|
31
|
+
* a half-arrived tool call is not something a consumer can do anything with,
|
|
32
|
+
* and `BrainChunk` says `{ text }` precisely so that stays true.
|
|
33
|
+
*/
|
|
34
|
+
import type { BrainChunk } from "../core/types.js";
|
|
35
|
+
/** Where chunks go. Exactly `BrainRunInput.onChunk`, named for readability. */
|
|
36
|
+
export type ChunkSink = (chunk: BrainChunk) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Read a provider stream to the end, emitting text as it arrives, and return
|
|
39
|
+
* every raw event for the accumulator.
|
|
40
|
+
*
|
|
41
|
+
* **Abort stops the consumption, and no chunk follows it.** The check sits
|
|
42
|
+
* before the emit, not after, so an abort that lands between two events ends
|
|
43
|
+
* the stream silently — the loop's `runWithTimeout` has already rejected by
|
|
44
|
+
* then and whatever is returned here is discarded. What must never happen is a
|
|
45
|
+
* chunk arriving at a consumer that has been told the call is over.
|
|
46
|
+
*
|
|
47
|
+
* Buffering every event is deliberate: one model response is small, and the
|
|
48
|
+
* accumulator that has to see all of them is the same function the tests feed
|
|
49
|
+
* by hand. A fold that ran only live would be a second implementation.
|
|
50
|
+
*/
|
|
51
|
+
export declare function consumeStream<T>(source: AsyncIterable<T>, deltaOf: (event: T) => string, sink: ChunkSink, signal?: AbortSignal): Promise<T[]>;
|
|
52
|
+
/** One `chat.completions` stream chunk, reduced to what we consume. */
|
|
53
|
+
export interface OpenAIStreamChunk {
|
|
54
|
+
choices?: Array<{
|
|
55
|
+
delta?: {
|
|
56
|
+
content?: string | null;
|
|
57
|
+
tool_calls?: Array<{
|
|
58
|
+
index: number;
|
|
59
|
+
function?: {
|
|
60
|
+
name?: string;
|
|
61
|
+
arguments?: string;
|
|
62
|
+
};
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
66
|
+
usage?: {
|
|
67
|
+
prompt_tokens?: number;
|
|
68
|
+
completion_tokens?: number;
|
|
69
|
+
} | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* What the OpenAI brain reads off a completion — streamed or not.
|
|
73
|
+
*
|
|
74
|
+
* `tool_calls` is `unknown[]` on purpose. OpenAI's own union now holds a CUSTOM
|
|
75
|
+
* tool call that carries no `function` at all, so no structural type both
|
|
76
|
+
* accepts the real `ChatCompletion` and promises a `function` field. The brain
|
|
77
|
+
* has narrowed each entry with its own cast since long before streaming
|
|
78
|
+
* existed — this simply stops pretending otherwise, and the non-streaming path
|
|
79
|
+
* needs no cast to satisfy it.
|
|
80
|
+
*/
|
|
81
|
+
export interface OpenAICompletion {
|
|
82
|
+
choices: Array<{
|
|
83
|
+
message: {
|
|
84
|
+
content?: string | null;
|
|
85
|
+
tool_calls?: unknown[];
|
|
86
|
+
};
|
|
87
|
+
}>;
|
|
88
|
+
usage?: {
|
|
89
|
+
prompt_tokens?: number;
|
|
90
|
+
completion_tokens?: number;
|
|
91
|
+
} | null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The text this chunk carries, or "" for a tool-call or usage-only chunk.
|
|
95
|
+
*
|
|
96
|
+
* **Why no fold here.** Two multiplicities are worth ruling out by name, since
|
|
97
|
+
* an extractor that silently reads "the first of several" is the exact bug this
|
|
98
|
+
* file was written to make impossible:
|
|
99
|
+
*
|
|
100
|
+
* - WITHIN a choice, `delta.content` is one string, never a list. Nothing to
|
|
101
|
+
* concatenate.
|
|
102
|
+
* - ACROSS choices, `choices` IS an array — but only when a request sets
|
|
103
|
+
* `n > 1`, which this brain never does. And if one ever did, several
|
|
104
|
+
* choices are ALTERNATIVE answers, not pieces of one: summing them would
|
|
105
|
+
* splice two different replies together. So `[0]` is right, and the rule
|
|
106
|
+
* that actually matters is that `accumulateOpenAI` reads `[0]` too — what
|
|
107
|
+
* the user watched is what the payload ends up holding, always.
|
|
108
|
+
*/
|
|
109
|
+
export declare function openAIDelta(chunk: OpenAIStreamChunk): string;
|
|
110
|
+
/**
|
|
111
|
+
* Fold OpenAI chunks into the completion they describe.
|
|
112
|
+
*
|
|
113
|
+
* Tool calls arrive as fragments keyed by `index`: the name once, the arguments
|
|
114
|
+
* in pieces to be concatenated into the JSON string the non-streamed API hands
|
|
115
|
+
* over whole. Keyed by index and re-sorted rather than pushed in arrival order,
|
|
116
|
+
* because the model interleaves parallel calls and the ORDER of calls is the
|
|
117
|
+
* one thing س٤ made load-bearing.
|
|
118
|
+
*
|
|
119
|
+
* `content` stays `null` when no text delta ever arrived — that is what the
|
|
120
|
+
* non-streaming API returns for a pure tool-call response, and the two spellings
|
|
121
|
+
* must not differ.
|
|
122
|
+
*/
|
|
123
|
+
export declare function accumulateOpenAI(chunks: readonly OpenAIStreamChunk[]): OpenAICompletion;
|
|
124
|
+
/**
|
|
125
|
+
* One Messages-API stream event, reduced to what we consume.
|
|
126
|
+
*
|
|
127
|
+
* `delta` is `unknown` for the same reason OpenAI's `tool_calls` is: the event
|
|
128
|
+
* union puts two unrelated shapes through one field name — a `message_delta`
|
|
129
|
+
* carries stop reason and stop sequence, a `content_block_delta` carries text
|
|
130
|
+
* or JSON fragments — and they share no property. Narrowing happens at the one
|
|
131
|
+
* place it is read (`blockDelta` below), which keeps the cast next to the check
|
|
132
|
+
* that justifies it and lets `RawMessageStreamEvent` flow in untouched.
|
|
133
|
+
*/
|
|
134
|
+
export interface AnthropicStreamEvent {
|
|
135
|
+
type: string;
|
|
136
|
+
index?: number;
|
|
137
|
+
message?: {
|
|
138
|
+
usage?: {
|
|
139
|
+
input_tokens?: number;
|
|
140
|
+
output_tokens?: number;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
content_block?: {
|
|
144
|
+
type?: string;
|
|
145
|
+
name?: string;
|
|
146
|
+
input?: unknown;
|
|
147
|
+
};
|
|
148
|
+
delta?: unknown;
|
|
149
|
+
usage?: {
|
|
150
|
+
output_tokens?: number | null;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** What the Anthropic brain reads off a message — streamed or not. */
|
|
154
|
+
export interface AnthropicMessage {
|
|
155
|
+
content: Array<{
|
|
156
|
+
type: string;
|
|
157
|
+
text?: string;
|
|
158
|
+
name?: string;
|
|
159
|
+
input?: unknown;
|
|
160
|
+
}>;
|
|
161
|
+
usage: {
|
|
162
|
+
input_tokens: number;
|
|
163
|
+
output_tokens: number;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The text this event carries, or "" for anything else (thinking included).
|
|
168
|
+
*
|
|
169
|
+
* **Why no fold here.** One `content_block_delta` carries exactly one `delta`
|
|
170
|
+
* for exactly one `index` — the event type is `{ delta, index, type }`, not a
|
|
171
|
+
* list — so there is never more than one piece of text in an event and nothing
|
|
172
|
+
* to concatenate. Multiplicity in Claude's stream lives BETWEEN events, and
|
|
173
|
+
* that is `accumulateAnthropic`'s job, keyed by `index`.
|
|
174
|
+
*/
|
|
175
|
+
export declare function anthropicDelta(event: AnthropicStreamEvent): string;
|
|
176
|
+
/**
|
|
177
|
+
* Fold Messages-API events into the message they describe.
|
|
178
|
+
*
|
|
179
|
+
* Blocks are addressed by `index` and rebuilt in index order: `text_delta`
|
|
180
|
+
* pieces concatenate into the block's text, `input_json_delta` pieces
|
|
181
|
+
* concatenate into the JSON that non-streamed responses deliver already parsed
|
|
182
|
+
* as `input`.
|
|
183
|
+
*
|
|
184
|
+
* Usage arrives in two halves — `input_tokens` at `message_start`, the final
|
|
185
|
+
* `output_tokens` at `message_delta` — so both are taken where they appear,
|
|
186
|
+
* with the later `message_delta` count winning.
|
|
187
|
+
*
|
|
188
|
+
* A tool block that never receives a delta ends as `input: {}`, which is what
|
|
189
|
+
* a zero-argument tool call looks like unstreamed.
|
|
190
|
+
*/
|
|
191
|
+
export declare function accumulateAnthropic(events: readonly AnthropicStreamEvent[]): AnthropicMessage;
|
|
192
|
+
/**
|
|
193
|
+
* One part of a Gemini candidate — text or a function call.
|
|
194
|
+
*
|
|
195
|
+
* `args` is `unknown` rather than a record because that is what the SDK's own
|
|
196
|
+
* `FunctionCall.args` widens to (`object`), and the brain has always narrowed
|
|
197
|
+
* it with a cast at the point of use. Declaring it narrower here would only
|
|
198
|
+
* move the cast, not remove it.
|
|
199
|
+
*/
|
|
200
|
+
export interface GeminiPart {
|
|
201
|
+
text?: string;
|
|
202
|
+
functionCall?: {
|
|
203
|
+
name: string;
|
|
204
|
+
args?: unknown;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** What the Gemini brain reads off a response — streamed or not. */
|
|
208
|
+
export interface GeminiResponse {
|
|
209
|
+
candidates?: Array<{
|
|
210
|
+
content?: {
|
|
211
|
+
parts?: GeminiPart[];
|
|
212
|
+
};
|
|
213
|
+
}>;
|
|
214
|
+
usageMetadata?: {
|
|
215
|
+
promptTokenCount?: number;
|
|
216
|
+
candidatesTokenCount?: number;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The text this stream item carries, across ALL of its text parts.
|
|
221
|
+
*
|
|
222
|
+
* **This is the one extractor of the four that must fold**, and the fold is not
|
|
223
|
+
* defensive: Gemini's stream element carries an ARRAY of parts, and it really
|
|
224
|
+
* does put several — a sentence, a function call, a trailing sentence — into
|
|
225
|
+
* one candidate. Return `parts[0].text` and the watcher sees the first fragment
|
|
226
|
+
* of a paragraph while the payload holds all of it, which is the precise shape
|
|
227
|
+
* of "the stream lied to the user's face".
|
|
228
|
+
*/
|
|
229
|
+
export declare function geminiDelta(item: GeminiResponse): string;
|
|
230
|
+
/**
|
|
231
|
+
* Fold Gemini stream items into the response they describe.
|
|
232
|
+
*
|
|
233
|
+
* **The text parts MERGE into one.** This is the whole reason this function is
|
|
234
|
+
* not a flat concatenation of `parts` arrays: the brain finds its answer with
|
|
235
|
+
* `parts.find(p => "text" in p)`, so a response left as fifty little text parts
|
|
236
|
+
* would return the first syllable of the answer and drop the rest. One part in,
|
|
237
|
+
* one part out — the same shape `generateContent` returns.
|
|
238
|
+
*
|
|
239
|
+
* The merged text keeps the POSITION of the first text part it saw, so a
|
|
240
|
+
* response that opened with a function call still reads in emitted order.
|
|
241
|
+
* (Order between text and calls does not reach the payload — the brain collects
|
|
242
|
+
* calls by filter and text by find — but a shape that reorders parts for no
|
|
243
|
+
* reason is a shape that will eventually be believed.)
|
|
244
|
+
*
|
|
245
|
+
* Usage is cumulative in Gemini's stream: the last item that carries it wins.
|
|
246
|
+
*/
|
|
247
|
+
export declare function accumulateGemini(items: readonly GeminiResponse[]): GeminiResponse;
|
|
248
|
+
/** One NDJSON line off `/api/chat` with `stream: true`. */
|
|
249
|
+
export interface OllamaStreamLine {
|
|
250
|
+
message?: {
|
|
251
|
+
content?: string;
|
|
252
|
+
tool_calls?: Array<{
|
|
253
|
+
function?: {
|
|
254
|
+
name?: string;
|
|
255
|
+
arguments?: Record<string, unknown>;
|
|
256
|
+
};
|
|
257
|
+
}>;
|
|
258
|
+
};
|
|
259
|
+
prompt_eval_count?: number;
|
|
260
|
+
eval_count?: number;
|
|
261
|
+
}
|
|
262
|
+
/** What the Ollama brain reads off `/api/chat` — streamed or not. */
|
|
263
|
+
export interface OllamaChatResponse {
|
|
264
|
+
message?: {
|
|
265
|
+
content?: string;
|
|
266
|
+
tool_calls?: Array<{
|
|
267
|
+
function?: {
|
|
268
|
+
name?: string;
|
|
269
|
+
arguments?: Record<string, unknown>;
|
|
270
|
+
};
|
|
271
|
+
}>;
|
|
272
|
+
};
|
|
273
|
+
prompt_eval_count?: number;
|
|
274
|
+
eval_count?: number;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The text this line carries.
|
|
278
|
+
*
|
|
279
|
+
* **Why no fold here.** One NDJSON line carries one `message`, and that message
|
|
280
|
+
* carries one `content` string — neither is a list, so there is nothing to
|
|
281
|
+
* concatenate within a line. Ollama's multiplicity is one token per LINE, which
|
|
282
|
+
* is `accumulateOllama`'s job.
|
|
283
|
+
*/
|
|
284
|
+
export declare function ollamaDelta(line: OllamaStreamLine): string;
|
|
285
|
+
/**
|
|
286
|
+
* Split whatever has arrived into whole NDJSON lines plus the leftover.
|
|
287
|
+
*
|
|
288
|
+
* Pure, and separate from the reader, because the bug this prevents is invisible
|
|
289
|
+
* in a happy-path test: a chunk boundary can fall anywhere, including the middle
|
|
290
|
+
* of a JSON object, and a splitter that forgets `rest` silently drops the token
|
|
291
|
+
* that straddled the seam. Blank lines are dropped; the trailing fragment is
|
|
292
|
+
* always handed back to be prefixed onto the next read.
|
|
293
|
+
*/
|
|
294
|
+
export declare function splitNdjsonFrame(buffer: string): {
|
|
295
|
+
lines: string[];
|
|
296
|
+
rest: string;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Fold NDJSON lines into the single response `stream: false` would return.
|
|
300
|
+
*
|
|
301
|
+
* Ollama streams content token by token, but hands over tool calls whole (with
|
|
302
|
+
* `arguments` already an object, not a JSON string) — so calls are collected,
|
|
303
|
+
* never concatenated. Counts arrive on the final line; last one wins.
|
|
304
|
+
*/
|
|
305
|
+
export declare function accumulateOllama(lines: readonly OllamaStreamLine[]): OllamaChatResponse;
|
|
306
|
+
/**
|
|
307
|
+
* Read an NDJSON body to the end, emitting text as it arrives.
|
|
308
|
+
*
|
|
309
|
+
* The Ollama brain owns no SDK, so this is the equivalent of `consumeStream`
|
|
310
|
+
* for a raw `fetch` body: decode, split on newlines, parse, emit, keep the
|
|
311
|
+
* remainder. A line that will not parse is skipped rather than thrown — a
|
|
312
|
+
* local server that emits a stray keep-alive should not fail a run — and the
|
|
313
|
+
* abort check sits before the emit for the same reason it does upstream.
|
|
314
|
+
*/
|
|
315
|
+
export declare function consumeNdjson(body: AsyncIterable<Uint8Array> | ReadableStream<Uint8Array>, sink: ChunkSink, signal?: AbortSignal): Promise<OllamaStreamLine[]>;
|