@msm-core/mini 0.4.0 → 0.5.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 +62 -0
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/adapters/index.d.ts +2 -1
- package/dist/adapters/index.js +2 -1
- package/dist/adapters/memory-redis.d.ts +38 -0
- package/dist/adapters/memory-redis.js +131 -0
- package/dist/adapters/redis-control.d.ts +0 -1
- package/dist/adapters/redis-control.js +0 -1
- package/dist/adapters/redis-lock.d.ts +0 -1
- package/dist/adapters/redis-lock.js +0 -1
- package/dist/adapters/redis-memory.d.ts +0 -1
- package/dist/adapters/redis-memory.js +0 -1
- package/dist/adapters/redis-types.d.ts +0 -1
- package/dist/adapters/redis-types.js +0 -1
- package/dist/brain/anthropic.d.ts +0 -1
- package/dist/brain/anthropic.js +15 -13
- package/dist/brain/factory.d.ts +0 -1
- package/dist/brain/factory.js +0 -1
- package/dist/brain/gemini.d.ts +0 -1
- package/dist/brain/gemini.js +11 -10
- package/dist/brain/ollama.d.ts +0 -1
- package/dist/brain/ollama.js +40 -7
- package/dist/brain/openai.d.ts +0 -1
- package/dist/brain/openai.js +6 -29
- package/dist/brain/pricing.d.ts +0 -1
- package/dist/brain/pricing.js +0 -1
- package/dist/brain/retry.d.ts +0 -1
- package/dist/brain/retry.js +0 -1
- package/dist/brain/tool-context.d.ts +37 -0
- package/dist/brain/tool-context.js +36 -0
- package/dist/brain/types.d.ts +0 -1
- package/dist/brain/types.js +0 -1
- package/dist/bridge/pipeline.d.ts +0 -1
- package/dist/bridge/pipeline.js +0 -1
- package/dist/core/context-builder.d.ts +0 -1
- package/dist/core/context-builder.js +0 -1
- package/dist/core/gates.d.ts +0 -1
- package/dist/core/gates.js +0 -1
- package/dist/core/guards.d.ts +3 -2
- package/dist/core/guards.js +24 -8
- package/dist/core/hooks.d.ts +0 -1
- package/dist/core/hooks.js +0 -1
- package/dist/core/loop.d.ts +0 -1
- package/dist/core/loop.js +5 -2
- package/dist/core/types.d.ts +7 -1
- package/dist/core/types.js +0 -1
- package/dist/definition/parser.d.ts +0 -1
- package/dist/definition/parser.js +13 -8
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/quality/scorer.d.ts +0 -1
- package/dist/quality/scorer.js +0 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/tools/dedup.d.ts +0 -1
- package/dist/tools/dedup.js +0 -1
- package/dist/tools/executor.d.ts +0 -1
- package/dist/tools/executor.js +21 -4
- package/package.json +2 -7
- package/dist/adapters/index.d.ts.map +0 -1
- package/dist/adapters/index.js.map +0 -1
- package/dist/adapters/redis-control.d.ts.map +0 -1
- package/dist/adapters/redis-control.js.map +0 -1
- package/dist/adapters/redis-lock.d.ts.map +0 -1
- package/dist/adapters/redis-lock.js.map +0 -1
- package/dist/adapters/redis-memory.d.ts.map +0 -1
- package/dist/adapters/redis-memory.js.map +0 -1
- package/dist/adapters/redis-types.d.ts.map +0 -1
- package/dist/adapters/redis-types.js.map +0 -1
- package/dist/brain/anthropic.d.ts.map +0 -1
- package/dist/brain/anthropic.js.map +0 -1
- package/dist/brain/factory.d.ts.map +0 -1
- package/dist/brain/factory.js.map +0 -1
- package/dist/brain/gemini.d.ts.map +0 -1
- package/dist/brain/gemini.js.map +0 -1
- package/dist/brain/ollama.d.ts.map +0 -1
- package/dist/brain/ollama.js.map +0 -1
- package/dist/brain/openai.d.ts.map +0 -1
- package/dist/brain/openai.js.map +0 -1
- package/dist/brain/pricing.d.ts.map +0 -1
- package/dist/brain/pricing.js.map +0 -1
- package/dist/brain/retry.d.ts.map +0 -1
- package/dist/brain/retry.js.map +0 -1
- package/dist/brain/types.d.ts.map +0 -1
- package/dist/brain/types.js.map +0 -1
- package/dist/bridge/pipeline.d.ts.map +0 -1
- package/dist/bridge/pipeline.js.map +0 -1
- package/dist/core/context-builder.d.ts.map +0 -1
- package/dist/core/context-builder.js.map +0 -1
- package/dist/core/gates.d.ts.map +0 -1
- package/dist/core/gates.js.map +0 -1
- package/dist/core/guards.d.ts.map +0 -1
- package/dist/core/guards.js.map +0 -1
- package/dist/core/hooks.d.ts.map +0 -1
- package/dist/core/hooks.js.map +0 -1
- package/dist/core/loop.d.ts.map +0 -1
- package/dist/core/loop.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/definition/parser.d.ts.map +0 -1
- package/dist/definition/parser.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/quality/scorer.d.ts.map +0 -1
- package/dist/quality/scorer.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/tools/dedup.d.ts.map +0 -1
- package/dist/tools/dedup.js.map +0 -1
- package/dist/tools/executor.d.ts.map +0 -1
- package/dist/tools/executor.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,68 @@ Follows [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [0.5.0] — 2026-06-30
|
|
9
|
+
|
|
10
|
+
Brain-parity + guard-integrity release from the 2026-06 SDK audit (H1, H2, H10, M1).
|
|
11
|
+
Every fix ships with a test.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **(H1) The Anthropic brain now folds prior tool results into the turn.** Only
|
|
16
|
+
OpenAI/Gemini did this; Anthropic re-sent just the raw message, so Claude never
|
|
17
|
+
saw what its tool calls returned, re-emitted `use_tool` every iteration to the
|
|
18
|
+
guard cap, and the loop's force-finalize answer was ungrounded. All four brains
|
|
19
|
+
now share one helper (`brain/tool-context.ts`) for both the fold+finalize-nudge
|
|
20
|
+
and the tool→JSON-Schema mapping. The Anthropic mapping also **no longer drops
|
|
21
|
+
`enum`** — enum-constrained parameters were previously unconstrained there.
|
|
22
|
+
- **(H2) The Ollama brain now supports tools.** It ignored `input.tools` entirely
|
|
23
|
+
and hard-coded `action:"respond"`, so tool-capable agents on `provider:"ollama"`
|
|
24
|
+
silently never called a tool — a capability loss precisely on the sovereign /
|
|
25
|
+
air-gapped path. It now sends the OpenAI-style `tools` schema to `/api/chat` and
|
|
26
|
+
parses `message.tool_calls` into a `use_tool` orchestration.
|
|
27
|
+
- **(H10) Guard limits no longer silently disable on `NaN`/garbage.** `resolveGuards`
|
|
28
|
+
merged with `??`, which only falls back on null/undefined — so a `NaN` (e.g. a
|
|
29
|
+
typo'd agent.md limit → `Number("foo")`), `Infinity`, or a non-number slipped
|
|
30
|
+
through, and `iteration >= NaN` / `cost > NaN` / `timeout > NaN` are all `false`,
|
|
31
|
+
turning those caps OFF. Values are now coerced with `Number.isFinite`, fall back
|
|
32
|
+
to the default when non-finite, and are **clamped to sane bounds** (so an absurd
|
|
33
|
+
override — in-process or over HTTP — can't remove a cap either). The definition
|
|
34
|
+
parser hardens its `## Limits` numbers the same way (`safeNum`).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **(M1) The agent.md `## Limits` block is now applied.** It was parsed into
|
|
39
|
+
`AgentDefinition.limits` but never reached the runtime guards (`resolveGuards`
|
|
40
|
+
read only `config.guards`), so documented limits silently no-op'd. Guard
|
|
41
|
+
precedence is now: **defaults < `## Limits` < explicit `config.guards` <
|
|
42
|
+
per-event `guardsOverride`**. ⚠️ Behavior change: an agent.md that declares a
|
|
43
|
+
`## Limits` value not also set in `config.guards` will now have that limit take
|
|
44
|
+
effect. Set the same field in `config.guards` to override.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- **(H11) `InMemoryRedis` — a correct single-node `RedisLike`** for local /
|
|
49
|
+
no-Redis / sovereign / air-gapped deploys, exported from `@msm-core/mini/adapters`
|
|
50
|
+
(`createInMemoryRedis()`). The SDK previously shipped only Redis-backed adapters,
|
|
51
|
+
forcing consumers to hand-roll a shim — and those shims returned `"OK"` on every
|
|
52
|
+
`set` and ignored `NX`/`PX`, so the per-session `RedisDistributedLock` ALWAYS
|
|
53
|
+
"succeeded" and two concurrent turns on one session ran against shared history.
|
|
54
|
+
This adapter honors `SET … NX/XX/PX/EX`, lazily expires keys, and implements the
|
|
55
|
+
lock's compare-and-del / compare-and-pexpire `eval` scripts, so the single-flight
|
|
56
|
+
lock actually holds. (One process only; use real Redis for multi-replica.)
|
|
57
|
+
- **`{ substituteParams }` onBeforeTool directive** — the hook can now run the tool
|
|
58
|
+
with overridden, re-validated params (dedup keys off the new params). This is what
|
|
59
|
+
lets `@msm-core/gate`'s `approved_with_edits` apply an operator's edits in-flight;
|
|
60
|
+
previously the hook could only `proceed` (original params) or `substituteResult`.
|
|
61
|
+
- `brain/tool-context.ts` — `foldToolResults()` + `toolParamsToJsonSchema()`, the
|
|
62
|
+
shared per-provider tool-context helpers.
|
|
63
|
+
- Tests: Ollama tool-calling + fold (mocked fetch), the shared helpers, `resolveGuards`
|
|
64
|
+
NaN/Infinity/clamp/deliberate-0 cases, a loop-level proof that `## Limits` applies
|
|
65
|
+
(and `config.guards` overrides it), and `InMemoryRedis` incl. lock mutual exclusion.
|
|
66
|
+
78 → **103** tests.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
8
70
|
## [0.4.0] — 2026-06-10
|
|
9
71
|
|
|
10
72
|
Production-hardening release — the sdk is now the default in-house agent runtime.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2026 MSM / UTS. All rights reserved.
|
|
2
|
+
|
|
3
|
+
PROPRIETARY AND CONFIDENTIAL
|
|
4
|
+
|
|
5
|
+
This package (@msm-core/mini) and its contents (the "Software") are the proprietary and
|
|
6
|
+
confidential property of MSM / UTS ("the Owner").
|
|
7
|
+
|
|
8
|
+
No license, right, or permission is granted to any party to use, copy, modify,
|
|
9
|
+
merge, publish, distribute, sublicense, sell, or create derivative works of the
|
|
10
|
+
Software, in whole or in part, except under a separate written agreement signed
|
|
11
|
+
by the Owner.
|
|
12
|
+
|
|
13
|
+
This package is published to the public npm registry solely to enable
|
|
14
|
+
installation as a runtime dependency. Such publication is NOT a grant of any
|
|
15
|
+
license to its source. The package is "UNLICENSED" (proprietary) as declared in
|
|
16
|
+
its manifest. Reproducing, reverse engineering, or redistributing it is not
|
|
17
|
+
permitted.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
21
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
package/README.md
CHANGED
|
@@ -181,6 +181,59 @@ Guards are hard limits that terminate the loop and return a forced response. Con
|
|
|
181
181
|
|
|
182
182
|
---
|
|
183
183
|
|
|
184
|
+
## Tool Approval Flow
|
|
185
|
+
|
|
186
|
+
For destructive or sensitive tools (refunds, deletions, outbound messages),
|
|
187
|
+
mark the tool `requiresApproval: true` and provide an `onBeforeTool` hook.
|
|
188
|
+
The runtime **guarantees** the tool cannot execute un-approved:
|
|
189
|
+
|
|
190
|
+
- No hook configured → the call is **blocked** (failed result, loop continues).
|
|
191
|
+
- The hook throws → **fails closed** for `requiresApproval` tools (fails open
|
|
192
|
+
for ordinary tools, where the hook is just observability).
|
|
193
|
+
- The hook runs **before** the dedup cache — a previously-approved cached
|
|
194
|
+
result can never bypass a fresh approval check.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
const refundTool: Tool = {
|
|
198
|
+
name: "issue_refund",
|
|
199
|
+
description: "Refund a customer order",
|
|
200
|
+
parameters: { orderId: { type: "string", description: "Order id", required: true } },
|
|
201
|
+
requiresApproval: true, // ← enforced by the runtime, not decorative
|
|
202
|
+
async execute(args, meta) { /* ... */ },
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const agent = createAgent({
|
|
206
|
+
// ...
|
|
207
|
+
tools: [refundTool],
|
|
208
|
+
hooks: {
|
|
209
|
+
// Called before EVERY tool execution (incl. would-be cache hits).
|
|
210
|
+
async onBeforeTool(toolName, params, meta) {
|
|
211
|
+
if (toolName !== "issue_refund") return "proceed";
|
|
212
|
+
|
|
213
|
+
const approval = await approvalStore.check(meta.sessionId, toolName, params);
|
|
214
|
+
if (approval === "granted") return "proceed";
|
|
215
|
+
if (approval === "denied") return "skip";
|
|
216
|
+
|
|
217
|
+
// Park the call: report "pending" to the user, execute on a later turn
|
|
218
|
+
// once the approval resolves and the brain retries the tool.
|
|
219
|
+
await approvalStore.request(meta.sessionId, toolName, params);
|
|
220
|
+
return {
|
|
221
|
+
substituteResult: {
|
|
222
|
+
tool: toolName,
|
|
223
|
+
status: "ok",
|
|
224
|
+
result: { pending_approval: true, message: "Refund queued for human approval" },
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Returns: `"proceed"` (execute), `"skip"` (skip silently), or
|
|
233
|
+
`{ substituteResult }` (inject a result without executing).
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
184
237
|
## Redis Schema
|
|
185
238
|
|
|
186
239
|
```
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { SessionMetadata } from "./redis-memory.js";
|
|
|
6
6
|
export { RedisControlBus } from "./redis-control.js";
|
|
7
7
|
export { RedisDistributedLock } from "./redis-lock.js";
|
|
8
8
|
export type { LockHandle } from "./redis-lock.js";
|
|
9
|
+
/** Correct single-node RedisLike for local / no-Redis / sovereign deploys + tests. */
|
|
10
|
+
export { InMemoryRedis, createInMemoryRedis } from "./memory-redis.js";
|
|
9
11
|
/** The client shape RedisConfig.client expects — for wiring a custom client. */
|
|
10
12
|
export type { RedisLike } from "./redis-types.js";
|
|
11
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/adapters/index.js
CHANGED
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
export { RedisMemory, connectRedis } from "./redis-memory.js";
|
|
5
5
|
export { RedisControlBus } from "./redis-control.js";
|
|
6
6
|
export { RedisDistributedLock } from "./redis-lock.js";
|
|
7
|
-
|
|
7
|
+
/** Correct single-node RedisLike for local / no-Redis / sovereign deploys + tests. */
|
|
8
|
+
export { InMemoryRedis, createInMemoryRedis } from "./memory-redis.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory RedisLike — a CORRECT single-node implementation of the subset of
|
|
3
|
+
* Redis that @msm-core/mini uses, for local / no-Redis / sovereign / air-gapped
|
|
4
|
+
* deploys and for tests. It exists because the SDK previously shipped ONLY
|
|
5
|
+
* Redis-backed adapters, forcing consumers to hand-roll a `RedisLike` shim — and
|
|
6
|
+
* those shims got the lock wrong: they returned "OK" on every `set` and ignored
|
|
7
|
+
* `NX`/`PX`, so `RedisDistributedLock.acquire()` ALWAYS succeeded and two
|
|
8
|
+
* concurrent turns on the same session ran against shared history (the exact race
|
|
9
|
+
* the per-session lock exists to prevent).
|
|
10
|
+
*
|
|
11
|
+
* This implementation honors `SET ... NX/XX/PX/EX`, lazily expires keys, and
|
|
12
|
+
* implements the lock's compare-and-del / compare-and-pexpire `eval` scripts —
|
|
13
|
+
* so the single-flight session lock actually holds.
|
|
14
|
+
*
|
|
15
|
+
* Scope: ONE process. It is not distributed (no cross-process mutual exclusion);
|
|
16
|
+
* for multi-replica deployments use a real Redis. Within a process it is correct.
|
|
17
|
+
*/
|
|
18
|
+
import type { RedisLike } from "./redis-types.js";
|
|
19
|
+
export declare class InMemoryRedis implements RedisLike {
|
|
20
|
+
private kv;
|
|
21
|
+
private lists;
|
|
22
|
+
private hashes;
|
|
23
|
+
/** Return the live entry for a key, lazily evicting it if its TTL has passed. */
|
|
24
|
+
private alive;
|
|
25
|
+
get(key: string): Promise<string | null>;
|
|
26
|
+
set(...args: unknown[]): Promise<unknown>;
|
|
27
|
+
setex(key: string, seconds: number, value: string): Promise<unknown>;
|
|
28
|
+
del(key: string | string[]): Promise<unknown>;
|
|
29
|
+
rpush(key: string, ...values: string[]): Promise<unknown>;
|
|
30
|
+
lrange(key: string, start: number, stop: number): Promise<string[]>;
|
|
31
|
+
ltrim(key: string, start: number, stop: number): Promise<unknown>;
|
|
32
|
+
expire(key: string, seconds: number): Promise<unknown>;
|
|
33
|
+
hget(key: string, field: string): Promise<string | null>;
|
|
34
|
+
hset(key: string, field: string, value: string): Promise<unknown>;
|
|
35
|
+
eval(script: string, numkeys: number, ...args: unknown[]): Promise<unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** Convenience factory — `redis: { client: createInMemoryRedis() }`. */
|
|
38
|
+
export declare function createInMemoryRedis(): InMemoryRedis;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** Normalize Redis-style list indices (supports negatives) to a slice range. */
|
|
2
|
+
function sliceRange(len, start, stop) {
|
|
3
|
+
let s = start < 0 ? Math.max(len + start, 0) : start;
|
|
4
|
+
let e = stop < 0 ? len + stop : Math.min(stop, len - 1);
|
|
5
|
+
if (s < 0)
|
|
6
|
+
s = 0;
|
|
7
|
+
return [s, e];
|
|
8
|
+
}
|
|
9
|
+
export class InMemoryRedis {
|
|
10
|
+
kv = new Map();
|
|
11
|
+
lists = new Map();
|
|
12
|
+
hashes = new Map();
|
|
13
|
+
/** Return the live entry for a key, lazily evicting it if its TTL has passed. */
|
|
14
|
+
alive(key) {
|
|
15
|
+
const e = this.kv.get(key);
|
|
16
|
+
if (!e)
|
|
17
|
+
return null;
|
|
18
|
+
if (e.expiresAt !== null && e.expiresAt <= Date.now()) {
|
|
19
|
+
this.kv.delete(key);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return e;
|
|
23
|
+
}
|
|
24
|
+
async get(key) {
|
|
25
|
+
return this.alive(key)?.value ?? null;
|
|
26
|
+
}
|
|
27
|
+
// Supports `set(k, v)`, `set(k, v, "EX", s)`, `set(k, v, "PX", ms)`, and the
|
|
28
|
+
// lock's `set(k, token, "PX", ms, "NX")`. Flags may appear in any order.
|
|
29
|
+
async set(...args) {
|
|
30
|
+
const [key, value, ...opts] = args;
|
|
31
|
+
let px = null;
|
|
32
|
+
let nx = false;
|
|
33
|
+
let xx = false;
|
|
34
|
+
for (let i = 0; i < opts.length; i++) {
|
|
35
|
+
const flag = String(opts[i]).toUpperCase();
|
|
36
|
+
if (flag === "PX")
|
|
37
|
+
px = Number(opts[++i]);
|
|
38
|
+
else if (flag === "EX")
|
|
39
|
+
px = Number(opts[++i]) * 1000;
|
|
40
|
+
else if (flag === "NX")
|
|
41
|
+
nx = true;
|
|
42
|
+
else if (flag === "XX")
|
|
43
|
+
xx = true;
|
|
44
|
+
}
|
|
45
|
+
const existing = this.alive(key);
|
|
46
|
+
if (nx && existing)
|
|
47
|
+
return null; // NX: set only if absent → acquire fails when held
|
|
48
|
+
if (xx && !existing)
|
|
49
|
+
return null; // XX: set only if present
|
|
50
|
+
this.kv.set(key, { value, expiresAt: px !== null && Number.isFinite(px) ? Date.now() + px : null });
|
|
51
|
+
return "OK";
|
|
52
|
+
}
|
|
53
|
+
async setex(key, seconds, value) {
|
|
54
|
+
this.kv.set(key, { value, expiresAt: Date.now() + seconds * 1000 });
|
|
55
|
+
return "OK";
|
|
56
|
+
}
|
|
57
|
+
async del(key) {
|
|
58
|
+
let n = 0;
|
|
59
|
+
for (const k of Array.isArray(key) ? key : [key]) {
|
|
60
|
+
if (this.kv.delete(k))
|
|
61
|
+
n++;
|
|
62
|
+
this.lists.delete(k);
|
|
63
|
+
this.hashes.delete(k);
|
|
64
|
+
}
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
async rpush(key, ...values) {
|
|
68
|
+
const list = this.lists.get(key) ?? [];
|
|
69
|
+
list.push(...values);
|
|
70
|
+
this.lists.set(key, list);
|
|
71
|
+
return list.length;
|
|
72
|
+
}
|
|
73
|
+
async lrange(key, start, stop) {
|
|
74
|
+
const list = this.lists.get(key) ?? [];
|
|
75
|
+
const [s, e] = sliceRange(list.length, start, stop);
|
|
76
|
+
if (s > e || s >= list.length)
|
|
77
|
+
return [];
|
|
78
|
+
return list.slice(s, e + 1);
|
|
79
|
+
}
|
|
80
|
+
async ltrim(key, start, stop) {
|
|
81
|
+
const list = this.lists.get(key) ?? [];
|
|
82
|
+
const [s, e] = sliceRange(list.length, start, stop);
|
|
83
|
+
this.lists.set(key, s > e || s >= list.length ? [] : list.slice(s, e + 1));
|
|
84
|
+
return "OK";
|
|
85
|
+
}
|
|
86
|
+
async expire(key, seconds) {
|
|
87
|
+
const e = this.alive(key);
|
|
88
|
+
if (!e)
|
|
89
|
+
return 0;
|
|
90
|
+
e.expiresAt = Date.now() + seconds * 1000;
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
async hget(key, field) {
|
|
94
|
+
return this.hashes.get(key)?.get(field) ?? null;
|
|
95
|
+
}
|
|
96
|
+
async hset(key, field, value) {
|
|
97
|
+
const h = this.hashes.get(key) ?? new Map();
|
|
98
|
+
const isNew = !h.has(field);
|
|
99
|
+
h.set(field, value);
|
|
100
|
+
this.hashes.set(key, h);
|
|
101
|
+
return isNew ? 1 : 0;
|
|
102
|
+
}
|
|
103
|
+
// The two Lua scripts the lock uses are compare-and-act on a single key. We
|
|
104
|
+
// recognize them by content (the only eval the runtime issues) and implement
|
|
105
|
+
// the same check-token-then-act semantics so release()/extend() are safe.
|
|
106
|
+
async eval(script, numkeys, ...args) {
|
|
107
|
+
const keys = args.slice(0, numkeys);
|
|
108
|
+
const argv = args.slice(numkeys);
|
|
109
|
+
const key = keys[0];
|
|
110
|
+
if (!key)
|
|
111
|
+
return 0;
|
|
112
|
+
const token = argv[0];
|
|
113
|
+
const current = this.alive(key)?.value ?? null;
|
|
114
|
+
if (current !== token)
|
|
115
|
+
return 0; // token mismatch → not our lock; do nothing
|
|
116
|
+
if (script.includes("pexpire")) {
|
|
117
|
+
// EXTEND: compare-and-pexpire
|
|
118
|
+
const entry = this.kv.get(key);
|
|
119
|
+
if (entry)
|
|
120
|
+
entry.expiresAt = Date.now() + Number(argv[1]);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
// RELEASE: compare-and-del
|
|
124
|
+
this.kv.delete(key);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Convenience factory — `redis: { client: createInMemoryRedis() }`. */
|
|
129
|
+
export function createInMemoryRedis() {
|
|
130
|
+
return new InMemoryRedis();
|
|
131
|
+
}
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { computeCostUsd } from "./pricing.js";
|
|
6
6
|
import { withRetry } from "./retry.js";
|
|
7
|
+
import { foldToolResults, toolParamsToJsonSchema } from "./tool-context.js";
|
|
7
8
|
export function createAnthropicBrain(opts) {
|
|
8
9
|
// claude-3-5-sonnet-20241022 was retired (2025-10-28); claude-sonnet-4-6 is
|
|
9
10
|
// its current drop-in replacement.
|
|
@@ -11,7 +12,14 @@ export function createAnthropicBrain(opts) {
|
|
|
11
12
|
return {
|
|
12
13
|
name: "anthropic",
|
|
13
14
|
async run(input) {
|
|
14
|
-
|
|
15
|
+
let Anthropic;
|
|
16
|
+
try {
|
|
17
|
+
({ default: Anthropic } = await import("@anthropic-ai/sdk"));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error("Anthropic selected but @anthropic-ai/sdk is not installed — " +
|
|
21
|
+
"install it or use a different provider.");
|
|
22
|
+
}
|
|
15
23
|
const apiKey = opts.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
16
24
|
if (!apiKey)
|
|
17
25
|
throw new Error("msm-mini: ANTHROPIC_API_KEY not set");
|
|
@@ -20,21 +28,16 @@ export function createAnthropicBrain(opts) {
|
|
|
20
28
|
role: m.role === "assistant" ? "assistant" : "user",
|
|
21
29
|
content: m.content,
|
|
22
30
|
}));
|
|
23
|
-
|
|
31
|
+
// Fold prior tool results into the current turn so Claude sees what its
|
|
32
|
+
// tool calls returned (and is nudged to finalize) — without this it
|
|
33
|
+
// re-emits use_tool every iteration until the guard cap (H1).
|
|
34
|
+
messages.push({ role: "user", content: foldToolResults(input.raw, input.tool_results) });
|
|
24
35
|
const tools = input.tools.length > 0
|
|
25
36
|
? input.tools.map((t) => ({
|
|
26
37
|
name: t.name,
|
|
27
38
|
description: t.description,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
properties: Object.fromEntries(Object.entries(t.parameters).map(([k, v]) => [
|
|
31
|
-
k,
|
|
32
|
-
{ type: v.type, description: v.description },
|
|
33
|
-
])),
|
|
34
|
-
required: Object.entries(t.parameters)
|
|
35
|
-
.filter(([, v]) => v.required)
|
|
36
|
-
.map(([k]) => k),
|
|
37
|
-
},
|
|
39
|
+
// toolParamsToJsonSchema keeps `enum` (the inline mapping dropped it).
|
|
40
|
+
input_schema: toolParamsToJsonSchema(t.parameters),
|
|
38
41
|
}))
|
|
39
42
|
: undefined;
|
|
40
43
|
const baseParams = {
|
|
@@ -80,4 +83,3 @@ export function createAnthropicBrain(opts) {
|
|
|
80
83
|
},
|
|
81
84
|
};
|
|
82
85
|
}
|
|
83
|
-
//# sourceMappingURL=anthropic.js.map
|
package/dist/brain/factory.d.ts
CHANGED
package/dist/brain/factory.js
CHANGED
package/dist/brain/gemini.d.ts
CHANGED
package/dist/brain/gemini.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Peer dependency: @google/generative-ai >= 0.14.0
|
|
4
4
|
*/
|
|
5
5
|
import { withRetry } from "./retry.js";
|
|
6
|
-
import {
|
|
6
|
+
import { foldToolResults } from "./tool-context.js";
|
|
7
7
|
export function createGeminiBrain(opts) {
|
|
8
8
|
const model = opts.model ?? "gemini-2.5-flash";
|
|
9
9
|
return {
|
|
@@ -14,6 +14,14 @@ export function createGeminiBrain(opts) {
|
|
|
14
14
|
process.env["GOOGLE_API_KEY"];
|
|
15
15
|
if (!apiKey)
|
|
16
16
|
throw new Error("msm-mini: GEMINI_API_KEY not set");
|
|
17
|
+
let GoogleGenerativeAI;
|
|
18
|
+
try {
|
|
19
|
+
({ GoogleGenerativeAI } = await import("@google/generative-ai"));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error("Gemini selected but @google/generative-ai is not installed — " +
|
|
23
|
+
"install it or use a different provider.");
|
|
24
|
+
}
|
|
17
25
|
const genai = new GoogleGenerativeAI(apiKey);
|
|
18
26
|
const geminiModel = genai.getGenerativeModel({ model });
|
|
19
27
|
// Build Gemini contents array (user/model alternation)
|
|
@@ -100,15 +108,8 @@ function buildGeminiContents(input) {
|
|
|
100
108
|
const role = msg.role === "assistant" ? "model" : "user";
|
|
101
109
|
contents.push({ role, parts: [{ text: msg.content }] });
|
|
102
110
|
}
|
|
103
|
-
// Current user message
|
|
104
|
-
|
|
105
|
-
if (input.tool_results?.length) {
|
|
106
|
-
const resultSummary = input.tool_results
|
|
107
|
-
.map((r) => `[${r.tool}] ${r.status}: ${JSON.stringify(r.result ?? r.error)}`)
|
|
108
|
-
.join("\n");
|
|
109
|
-
userText = `Tool results:\n${resultSummary}\n\n${input.raw}`;
|
|
110
|
-
}
|
|
111
|
+
// Current user message — shared fold + finalize nudge (parity with all brains).
|
|
112
|
+
const userText = foldToolResults(input.raw, input.tool_results);
|
|
111
113
|
contents.push({ role: "user", parts: [{ text: userText }] });
|
|
112
114
|
return contents;
|
|
113
115
|
}
|
|
114
|
-
//# sourceMappingURL=gemini.js.map
|
package/dist/brain/ollama.d.ts
CHANGED
package/dist/brain/ollama.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* No peer dependencies — plain HTTP fetch.
|
|
4
4
|
*/
|
|
5
5
|
import { withRetry } from "./retry.js";
|
|
6
|
+
import { foldToolResults, toolParamsToJsonSchema } from "./tool-context.js";
|
|
6
7
|
export function createOllamaBrain(opts) {
|
|
7
8
|
const endpoint = opts.endpoint ?? process.env["OLLAMA_ENDPOINT"] ?? "http://localhost:11434";
|
|
8
9
|
const model = opts.model ?? "llama3.2";
|
|
@@ -12,29 +13,61 @@ export function createOllamaBrain(opts) {
|
|
|
12
13
|
const messages = [
|
|
13
14
|
{ role: "system", content: input.system_context },
|
|
14
15
|
...input.history.map((m) => ({ role: m.role, content: m.content })),
|
|
15
|
-
|
|
16
|
+
// Fold prior tool results into the turn (parity with the cloud brains).
|
|
17
|
+
{ role: "user", content: foldToolResults(input.raw, input.tool_results) },
|
|
16
18
|
];
|
|
19
|
+
// Ollama's /api/chat speaks the OpenAI-style `tools` schema (Ollama ≥0.3).
|
|
20
|
+
// Without this, tool-capable agents on provider:'ollama' silently never
|
|
21
|
+
// call a tool — a capability loss precisely on the sovereign/air-gapped
|
|
22
|
+
// path this provider exists to serve (H2).
|
|
23
|
+
const tools = input.tools.length > 0
|
|
24
|
+
? input.tools.map((t) => ({
|
|
25
|
+
type: "function",
|
|
26
|
+
function: {
|
|
27
|
+
name: t.name,
|
|
28
|
+
description: t.description,
|
|
29
|
+
parameters: toolParamsToJsonSchema(t.parameters),
|
|
30
|
+
},
|
|
31
|
+
}))
|
|
32
|
+
: undefined;
|
|
17
33
|
const response = await withRetry(() => fetch(`${endpoint}/api/chat`, {
|
|
18
34
|
method: "POST",
|
|
19
35
|
headers: { "Content-Type": "application/json" },
|
|
20
|
-
body: JSON.stringify({
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
model,
|
|
38
|
+
messages,
|
|
39
|
+
stream: false,
|
|
40
|
+
...(tools ? { tools } : {}),
|
|
41
|
+
}),
|
|
21
42
|
...(input.signal ? { signal: input.signal } : {}),
|
|
22
43
|
}), input.signal ? { signal: input.signal } : {});
|
|
23
44
|
if (!response.ok) {
|
|
24
45
|
throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
|
|
25
46
|
}
|
|
26
47
|
const data = (await response.json());
|
|
27
|
-
const text = data.message?.content ?? "";
|
|
28
48
|
const inputTokens = data.prompt_eval_count;
|
|
29
49
|
const outputTokens = data.eval_count;
|
|
50
|
+
const usageBlock = inputTokens !== undefined || outputTokens !== undefined
|
|
51
|
+
? { usage: { ...(inputTokens !== undefined ? { inputTokens } : {}), ...(outputTokens !== undefined ? { outputTokens } : {}) } }
|
|
52
|
+
: {};
|
|
53
|
+
// Tool call (Ollama returns arguments already parsed as an object, unlike
|
|
54
|
+
// OpenAI's JSON string).
|
|
55
|
+
const call = data.message?.tool_calls?.[0]?.function;
|
|
56
|
+
if (call?.name) {
|
|
57
|
+
const orchestration = {
|
|
58
|
+
action: "use_tool",
|
|
59
|
+
confidence: 0.9,
|
|
60
|
+
tool_name: call.name,
|
|
61
|
+
tool_params: call.arguments ?? {},
|
|
62
|
+
};
|
|
63
|
+
return { orchestration, ...usageBlock };
|
|
64
|
+
}
|
|
65
|
+
const text = data.message?.content ?? "";
|
|
30
66
|
return {
|
|
31
67
|
generation: { response_text: text },
|
|
32
68
|
orchestration: { action: "respond", confidence: 0.85 },
|
|
33
|
-
...
|
|
34
|
-
? { usage: { ...(inputTokens !== undefined ? { inputTokens } : {}), ...(outputTokens !== undefined ? { outputTokens } : {}) } }
|
|
35
|
-
: {}),
|
|
69
|
+
...usageBlock,
|
|
36
70
|
};
|
|
37
71
|
},
|
|
38
72
|
};
|
|
39
73
|
}
|
|
40
|
-
//# sourceMappingURL=ollama.js.map
|
package/dist/brain/openai.d.ts
CHANGED