@msm-core/mini 0.5.1 → 0.8.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/CHANGELOG.md +25 -0
- 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 +68 -35
- package/dist/brain/ollama.js +68 -19
- package/dist/brain/openai.js +57 -23
- package/dist/brain/pricing.js +1 -0
- 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/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 +223 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/package.json +11 -11
package/dist/core/types.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* All contracts for the lite agent runtime. Brain-agnostic, zero embedded
|
|
5
5
|
* databases. Application layer owns persistence; agent owns the loop.
|
|
6
6
|
*/
|
|
7
|
+
import type { SessionLogPort } from "@msm-core/session";
|
|
8
|
+
import type { ContextBudget } from "./context-builder.js";
|
|
7
9
|
/**
|
|
8
10
|
* A single memory entry — assembled by msm-context or by the app directly.
|
|
9
11
|
* The agent injects these into every brain call in priority order.
|
|
@@ -114,12 +116,67 @@ export interface BrainRunInput {
|
|
|
114
116
|
* safe — the loop still stops waiting via its timeout race.
|
|
115
117
|
*/
|
|
116
118
|
signal?: AbortSignal;
|
|
119
|
+
/**
|
|
120
|
+
* Present ONLY when a consumer asked to see the answer as it is produced
|
|
121
|
+
* (ب١). Its absence is the normal case and means "do not stream" — every
|
|
122
|
+
* built-in brain then walks the exact non-streaming path it always walked.
|
|
123
|
+
*
|
|
124
|
+
* **Display, not truth.** What the loop records, gates, returns and replays
|
|
125
|
+
* is still the completed `BrainPayload`. A chunk is never logged as an event,
|
|
126
|
+
* never enters a `replay` fingerprint, and never changes the session-log
|
|
127
|
+
* invariant. Drop every chunk on the floor and the run is bit-identical.
|
|
128
|
+
*
|
|
129
|
+
* A brain that ignores this field is CORRECT, not broken — it simply does not
|
|
130
|
+
* stream. Third-party brains written before ب١ keep working untouched.
|
|
131
|
+
*/
|
|
132
|
+
onChunk?: (chunk: BrainChunk) => void;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* One piece of a model answer as it arrives.
|
|
136
|
+
*
|
|
137
|
+
* **Text only in v1.** Streaming tool CALLS is deliberately out of scope: the
|
|
138
|
+
* brain still accumulates them and returns them in the payload exactly as it
|
|
139
|
+
* does today, so a half-arrived tool call is never observable and never
|
|
140
|
+
* executable. The type is an object rather than a bare `string` so that day —
|
|
141
|
+
* if it comes — adds a field instead of breaking every consumer's signature.
|
|
142
|
+
*/
|
|
143
|
+
export interface BrainChunk {
|
|
144
|
+
text: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* One tool invocation a model asked for, in the model's own order.
|
|
148
|
+
*
|
|
149
|
+
* The unit is the CALL, not the step: modern models emit several in one
|
|
150
|
+
* response, and a step-shaped type would force the loop back into one
|
|
151
|
+
* round-trip per tool — the cost this exists to remove.
|
|
152
|
+
*/
|
|
153
|
+
export interface BrainToolCall {
|
|
154
|
+
name: string;
|
|
155
|
+
params: Record<string, unknown>;
|
|
117
156
|
}
|
|
118
157
|
export interface BrainOrchestration {
|
|
119
158
|
action: "use_tool" | "respond" | "clarify" | "escalate" | "delegate";
|
|
120
159
|
confidence: number;
|
|
160
|
+
/**
|
|
161
|
+
* The FIRST call of the step. Always filled whenever `tool_calls` is —
|
|
162
|
+
* never left behind as `tool_calls` grows. Every consumer written against
|
|
163
|
+
* one-tool-per-step keeps reading exactly what it always read.
|
|
164
|
+
*/
|
|
121
165
|
tool_name?: string;
|
|
166
|
+
/** The first call's arguments. Same covenant as `tool_name`. */
|
|
122
167
|
tool_params?: Record<string, unknown>;
|
|
168
|
+
/**
|
|
169
|
+
* ALL the calls of this step, in the order the model emitted them.
|
|
170
|
+
*
|
|
171
|
+
* **A pure addition.** A brain that fills only `tool_name`/`tool_params`
|
|
172
|
+
* (every third-party brain written before this field existed, and every
|
|
173
|
+
* scripted brain in the suite) is normalized to a single-call step and takes
|
|
174
|
+
* the path it always took, character for character. A brain that fills this
|
|
175
|
+
* field fills the two above with `tool_calls[0]` as well — the covenant is
|
|
176
|
+
* one-directional: new readers may read the array, old readers never see a
|
|
177
|
+
* hole where the first call used to be.
|
|
178
|
+
*/
|
|
179
|
+
tool_calls?: BrainToolCall[];
|
|
123
180
|
reasoning?: string;
|
|
124
181
|
[key: string]: unknown;
|
|
125
182
|
}
|
|
@@ -253,6 +310,89 @@ export interface DocumentState {
|
|
|
253
310
|
startedAt: number;
|
|
254
311
|
updatedAt: number;
|
|
255
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Final run metadata persisted after every handle() — for ops dashboards.
|
|
315
|
+
*
|
|
316
|
+
* Declared here, with the port it belongs to, rather than inside one adapter:
|
|
317
|
+
* it is part of the contract every store implements, not a Redis detail. The
|
|
318
|
+
* Redis adapter re-exports it, so existing imports keep working unchanged.
|
|
319
|
+
*/
|
|
320
|
+
export interface SessionMetadata {
|
|
321
|
+
iterationCount: number;
|
|
322
|
+
startedAt: number;
|
|
323
|
+
totalCostUsd: number;
|
|
324
|
+
status: "running" | "completed" | "failed" | "killed";
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The session-memory PORT — conversation history, run metadata, document state.
|
|
328
|
+
*
|
|
329
|
+
* The loop talks to this interface and never to a concrete store. Inject an
|
|
330
|
+
* implementation via `AgentConfig.memory`; when it is omitted the loop builds
|
|
331
|
+
* the bundled `RedisMemory` from `AgentConfig.redis` exactly as it always has.
|
|
332
|
+
*
|
|
333
|
+
* Six functions — the surface `RedisMemory` already had, at its current
|
|
334
|
+
* signatures. Nothing is added speculatively: an append-only event log is a
|
|
335
|
+
* different contract and does not belong here.
|
|
336
|
+
*/
|
|
337
|
+
export interface SessionStore {
|
|
338
|
+
/** Append one message to this session's conversation history. */
|
|
339
|
+
appendHistory(sessionId: string, entry: Message): Promise<void>;
|
|
340
|
+
/** The last `limit` messages, oldest-first. Default limit: 50. */
|
|
341
|
+
getHistory(sessionId: string, limit?: number): Promise<Message[]>;
|
|
342
|
+
getMetadata(sessionId: string): Promise<SessionMetadata | null>;
|
|
343
|
+
setMetadata(sessionId: string, meta: SessionMetadata): Promise<void>;
|
|
344
|
+
getDocumentState(sessionId: string): Promise<DocumentState | null>;
|
|
345
|
+
setDocumentState(sessionId: string, state: DocumentState): Promise<void>;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* What a compactor decides when it decides to compact.
|
|
349
|
+
*
|
|
350
|
+
* `keepTurns` is a REQUEST, not a command: the loop hands it to
|
|
351
|
+
* `compactionBoundary`, which cuts only at whole turns and refuses to sever a
|
|
352
|
+
* tool call from its result. A compactor asking to keep zero turns of a session
|
|
353
|
+
* whose last turn is still unpaired gets a smaller range than it asked for, or
|
|
354
|
+
* none at all. That is the point of having two defences.
|
|
355
|
+
*/
|
|
356
|
+
export interface CompactionDecision {
|
|
357
|
+
/** The text the compacted range will read as. Empty means "no compaction". */
|
|
358
|
+
summary: string;
|
|
359
|
+
/** How many trailing turns to leave verbatim. Clamped to what is safe. */
|
|
360
|
+
keepTurns: number;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The compaction seat: a port the loop consults before it derives the
|
|
364
|
+
* conversation, and never a trick it plays on the history array.
|
|
365
|
+
*
|
|
366
|
+
* Today a long session is trimmed by `history.slice(-maxHistoryMessages)`. That
|
|
367
|
+
* cut lands wherever it lands — through the middle of a tool call and its
|
|
368
|
+
* result, through the middle of a turn — and it is silent. A long legal case in
|
|
369
|
+
* nisus loses its own beginning and nothing anywhere says so. This port is the
|
|
370
|
+
* seat where that stops being true.
|
|
371
|
+
*
|
|
372
|
+
* Three properties are contractual:
|
|
373
|
+
*
|
|
374
|
+
* 1. **It only works with `sessionLog`.** Compaction records a `compaction`
|
|
375
|
+
* event; without a log there is nowhere to record it, and a summary that is
|
|
376
|
+
* not recorded is exactly the silent rewrite this replaces. With `memory`
|
|
377
|
+
* alone the port is inert and the old `slice` path runs untouched — declared,
|
|
378
|
+
* not an oversight.
|
|
379
|
+
*
|
|
380
|
+
* 2. **`null` means no compaction.** The overwhelmingly common answer. A port
|
|
381
|
+
* that returns `null` costs one call and changes nothing.
|
|
382
|
+
*
|
|
383
|
+
* 3. **A throwing port fails the turn.** It is not swallowed. A compactor that
|
|
384
|
+
* has quietly been failing for a month is a session that has quietly been
|
|
385
|
+
* losing its head for a month — the failure this exists to end, wearing a
|
|
386
|
+
* different hat.
|
|
387
|
+
*
|
|
388
|
+
* @param messages the conversation DERIVED from the log so far, in order.
|
|
389
|
+
* @param budget the budget the derived history will actually be trimmed to —
|
|
390
|
+
* `maxHistoryMessages` is the effective cap, already reconciled
|
|
391
|
+
* with the agent's iteration-derived history limit.
|
|
392
|
+
*/
|
|
393
|
+
export interface CompactionPort {
|
|
394
|
+
maybeCompact(messages: readonly Message[], budget: ContextBudget): Promise<CompactionDecision | null>;
|
|
395
|
+
}
|
|
256
396
|
export interface RedisConfig {
|
|
257
397
|
/**
|
|
258
398
|
* Redis connection URL — mini creates its own connection.
|
|
@@ -304,6 +444,68 @@ export interface AgentConfig {
|
|
|
304
444
|
definition: string | AgentDefinition;
|
|
305
445
|
brain: Brain;
|
|
306
446
|
redis: RedisConfig;
|
|
447
|
+
/**
|
|
448
|
+
* Optional session-memory port. When injected, the loop routes ALL history /
|
|
449
|
+
* metadata / document-state access through it and never constructs a
|
|
450
|
+
* Redis-backed store. When omitted, the loop builds `RedisMemory` from
|
|
451
|
+
* `redis` exactly as before — same prefix, same TTLs, same tenant scoping —
|
|
452
|
+
* so every existing consumer is unaffected.
|
|
453
|
+
*
|
|
454
|
+
* `redis` stays required either way: the control bus, the session lock and
|
|
455
|
+
* tool dedup are separate ports and still ride on it.
|
|
456
|
+
*/
|
|
457
|
+
memory?: SessionStore;
|
|
458
|
+
/**
|
|
459
|
+
* Optional session **event log** (`@msm-core/session`). Injecting it inverts
|
|
460
|
+
* where conversation context comes from:
|
|
461
|
+
*
|
|
462
|
+
* - **Omitted (default)** — nothing changes. Not one event is written, the
|
|
463
|
+
* history handed to the brain is `memory.getHistory()` exactly as before,
|
|
464
|
+
* and the invariant below never runs. Every existing consumer is
|
|
465
|
+
* bit-for-bit unaffected.
|
|
466
|
+
* - **Injected** — the loop writes every event as it happens (the user
|
|
467
|
+
* message, each step, each model request/response, every tool call and
|
|
468
|
+
* its result, guards, the delivered reply), and the conversation handed
|
|
469
|
+
* to the model is DERIVED from the log (`deriveMessages`) instead of read
|
|
470
|
+
* from the history array. `memory.appendHistory` keeps being written too:
|
|
471
|
+
* one transitional phase of dual writing, log as truth, array as
|
|
472
|
+
* compatibility.
|
|
473
|
+
*
|
|
474
|
+
* Two consequences worth knowing before you inject one:
|
|
475
|
+
*
|
|
476
|
+
* 1. **The derived conversation is richer.** It carries the
|
|
477
|
+
* `assistant`/`tool` pairs of previous turns, which the history array
|
|
478
|
+
* never held. That is the point of the log, not a side effect — but it
|
|
479
|
+
* is a real change in what the model sees.
|
|
480
|
+
* 2. **It is the truth, so its failures are loud.** A rejected append
|
|
481
|
+
* (e.g. `SeqGapError`) fails the turn rather than being swallowed: a log
|
|
482
|
+
* with a hole in it cannot back an audit, a resume or a replay.
|
|
483
|
+
*
|
|
484
|
+
* Isolation is the injector's, exactly as with `memory`: this port takes a
|
|
485
|
+
* `sessionId` and knows nothing of tenants. Whoever injects a log injects its
|
|
486
|
+
* tenant scoping with it.
|
|
487
|
+
*/
|
|
488
|
+
sessionLog?: SessionLogPort;
|
|
489
|
+
/**
|
|
490
|
+
* Optional **compaction port** (ض١). Consulted once per turn, right before
|
|
491
|
+
* the conversation is derived, and only when `sessionLog` is also injected.
|
|
492
|
+
*
|
|
493
|
+
* - **Omitted (default)** — not one extra call, not one extra event. The
|
|
494
|
+
* conversation is derived and trimmed exactly as it was before.
|
|
495
|
+
* - **Omitted `sessionLog`** — the seat is inert even when this is set:
|
|
496
|
+
* compaction records an event, and without a log there is nothing to
|
|
497
|
+
* record it in. The legacy history path keeps its `slice`. Declared.
|
|
498
|
+
* - **Both injected** — when the port returns a summary, the loop writes a
|
|
499
|
+
* `compaction` event covering a safe range (`compactionBoundary`) and
|
|
500
|
+
* re-derives. The model then reads one summary plus the recent turns
|
|
501
|
+
* verbatim, and the invariant still proves that what it read came out of
|
|
502
|
+
* the log — because the compaction is IN the log.
|
|
503
|
+
*
|
|
504
|
+
* `createBrainCompactor` is a reference implementation that summarises with an
|
|
505
|
+
* injected `Brain`. The port does not require it; anything with a
|
|
506
|
+
* `maybeCompact` will do, including one that never calls a model at all.
|
|
507
|
+
*/
|
|
508
|
+
compaction?: CompactionPort;
|
|
307
509
|
/** App-provided tools — agent can only call tools listed here */
|
|
308
510
|
tools: Tool[];
|
|
309
511
|
guards?: GuardConfig;
|
|
@@ -343,6 +545,15 @@ export interface GuardFiredInfo {
|
|
|
343
545
|
signal: GuardSignal;
|
|
344
546
|
iteration: number;
|
|
345
547
|
}
|
|
548
|
+
/** One streamed piece of the model's answer, with the step it belongs to (ب١). */
|
|
549
|
+
export interface ChunkInfo {
|
|
550
|
+
sessionId: string;
|
|
551
|
+
/** The loop iteration whose brain call produced this text. */
|
|
552
|
+
iteration: number;
|
|
553
|
+
/** The delta — not the running total. Concatenating every chunk of a step
|
|
554
|
+
* yields the text that step's payload carries. */
|
|
555
|
+
text: string;
|
|
556
|
+
}
|
|
346
557
|
/**
|
|
347
558
|
* Return value from onBeforeTool hook:
|
|
348
559
|
* "proceed" — execute the tool normally
|
|
@@ -365,6 +576,18 @@ export interface AgentHooks {
|
|
|
365
576
|
onToolCall?: (info: ToolCallInfo) => void;
|
|
366
577
|
onGuard?: (info: GuardFiredInfo) => void;
|
|
367
578
|
onFatalError?: (error: Error, sessionId: string) => void;
|
|
579
|
+
/**
|
|
580
|
+
* Called for each piece of model text as it arrives (ب١). Wire it to an SSE
|
|
581
|
+
* channel and the user stops watching a silent screen.
|
|
582
|
+
*
|
|
583
|
+
* **Setting this is what turns streaming on.** The loop passes `onChunk` down
|
|
584
|
+
* to the brain only when this hook exists; with no hook, `BrainRunInput`
|
|
585
|
+
* carries no `onChunk` and every brain takes its ordinary non-streaming path.
|
|
586
|
+
*
|
|
587
|
+
* Fire-and-forget like its siblings: a throwing hook is swallowed and the
|
|
588
|
+
* stream continues — a broken display must not break the run.
|
|
589
|
+
*/
|
|
590
|
+
onChunk?: (info: ChunkInfo) => void;
|
|
368
591
|
/**
|
|
369
592
|
* Called before every tool execution (BEFORE the dedup check, so cached
|
|
370
593
|
* results can never bypass the gate). Return "proceed" to execute normally,
|
package/dist/index.d.ts
CHANGED
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
* import { RedisMemory, RedisControlBus } from 'msm-mini/adapters'
|
|
8
8
|
*/
|
|
9
9
|
export { createAgent } from "./core/loop.js";
|
|
10
|
+
export { createBrainCompactor } from "./core/loop.js";
|
|
11
|
+
export type { BrainCompactorOptions } from "./core/loop.js";
|
|
10
12
|
export { createGeminiBrain } from "./brain/gemini.js";
|
|
11
13
|
export { createOpenAIBrain } from "./brain/openai.js";
|
|
12
14
|
export { createAnthropicBrain } from "./brain/anthropic.js";
|
|
13
15
|
export { createOllamaBrain } from "./brain/ollama.js";
|
|
14
16
|
export { buildBrain } from "./brain/factory.js";
|
|
15
17
|
export { parseDefinition } from "./definition/parser.js";
|
|
16
|
-
export type { Agent, AgentConfig, AgentEvent, AgentContext, AgentDefinition, AgentHooks, BeforeToolHookResult, Brain, BrainPayload, BrainRunInput, DocumentState, GateConfig, GuardConfig, GuardSignal, GuardSignalType, IterationInfo, LoopOutcome, MemoryEntry, Message, OutcomeType, OutputValidation, OutputValidator, RedisConfig, RunState, SectionInfo, TenantContext, Tool, ToolCallInfo, ToolDefinition, ToolMeta, ToolParameter, ToolResult, } from "./core/types.js";
|
|
18
|
+
export type { Agent, AgentConfig, AgentEvent, AgentContext, AgentDefinition, AgentHooks, BeforeToolHookResult, Brain, BrainChunk, BrainOrchestration, BrainPayload, BrainRunInput, BrainToolCall, ChunkInfo, CompactionDecision, CompactionPort, DocumentState, GateConfig, GuardConfig, GuardSignal, GuardSignalType, IterationInfo, LoopOutcome, MemoryEntry, Message, OutcomeType, OutputValidation, OutputValidator, RedisConfig, RunState, SectionInfo, SessionMetadata, SessionStore, TenantContext, Tool, ToolCallInfo, ToolDefinition, ToolMeta, ToolParameter, ToolResult, } from "./core/types.js";
|
|
19
|
+
export type { ContextBudget } from "./core/context-builder.js";
|
|
17
20
|
export { resolveGuards, DEFAULT_GUARDS } from "./core/guards.js";
|
|
18
21
|
export { scoreOutcome } from "./quality/scorer.js";
|
|
19
22
|
export type { QualityScore, QualityFlag } from "./quality/scorer.js";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
// ── Main factory ────────────────────────────────────────────────────────────
|
|
10
10
|
export { createAgent } from "./core/loop.js";
|
|
11
|
+
// ── Compaction (ض١) — the seat, and one reference occupant ──────────────────
|
|
12
|
+
export { createBrainCompactor } from "./core/loop.js";
|
|
11
13
|
// ── Brain factories ─────────────────────────────────────────────────────────
|
|
12
14
|
export { createGeminiBrain } from "./brain/gemini.js";
|
|
13
15
|
export { createOpenAIBrain } from "./brain/openai.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msm-core/mini",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Portable AI agent execution loop — brain-agnostic, zero embedded databases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,13 +31,6 @@
|
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsc",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"test:watch": "vitest",
|
|
38
|
-
"clean": "rm -rf dist",
|
|
39
|
-
"prepublishOnly": "npm run build && npm test"
|
|
40
|
-
},
|
|
41
34
|
"peerDependencies": {
|
|
42
35
|
"openai": ">=4.0.0",
|
|
43
36
|
"@anthropic-ai/sdk": ">=0.20.0",
|
|
@@ -55,7 +48,8 @@
|
|
|
55
48
|
}
|
|
56
49
|
},
|
|
57
50
|
"dependencies": {
|
|
58
|
-
"ioredis": "^5.3.2"
|
|
51
|
+
"ioredis": "^5.3.2",
|
|
52
|
+
"@msm-core/session": "^0.2.0"
|
|
59
53
|
},
|
|
60
54
|
"devDependencies": {
|
|
61
55
|
"@types/node": "^20.0.0",
|
|
@@ -71,5 +65,11 @@
|
|
|
71
65
|
"portable"
|
|
72
66
|
],
|
|
73
67
|
"author": "Emad Jumaah",
|
|
74
|
-
"license": "UNLICENSED"
|
|
75
|
-
|
|
68
|
+
"license": "UNLICENSED",
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsc",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"test:watch": "vitest",
|
|
73
|
+
"clean": "rm -rf dist"
|
|
74
|
+
}
|
|
75
|
+
}
|