@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +53 -0
  4. package/dist/adapters/index.d.ts +2 -1
  5. package/dist/adapters/index.js +2 -1
  6. package/dist/adapters/memory-redis.d.ts +38 -0
  7. package/dist/adapters/memory-redis.js +131 -0
  8. package/dist/adapters/redis-control.d.ts +0 -1
  9. package/dist/adapters/redis-control.js +0 -1
  10. package/dist/adapters/redis-lock.d.ts +0 -1
  11. package/dist/adapters/redis-lock.js +0 -1
  12. package/dist/adapters/redis-memory.d.ts +0 -1
  13. package/dist/adapters/redis-memory.js +0 -1
  14. package/dist/adapters/redis-types.d.ts +0 -1
  15. package/dist/adapters/redis-types.js +0 -1
  16. package/dist/brain/anthropic.d.ts +0 -1
  17. package/dist/brain/anthropic.js +15 -13
  18. package/dist/brain/factory.d.ts +0 -1
  19. package/dist/brain/factory.js +0 -1
  20. package/dist/brain/gemini.d.ts +0 -1
  21. package/dist/brain/gemini.js +11 -10
  22. package/dist/brain/ollama.d.ts +0 -1
  23. package/dist/brain/ollama.js +40 -7
  24. package/dist/brain/openai.d.ts +0 -1
  25. package/dist/brain/openai.js +6 -29
  26. package/dist/brain/pricing.d.ts +0 -1
  27. package/dist/brain/pricing.js +0 -1
  28. package/dist/brain/retry.d.ts +0 -1
  29. package/dist/brain/retry.js +0 -1
  30. package/dist/brain/tool-context.d.ts +37 -0
  31. package/dist/brain/tool-context.js +36 -0
  32. package/dist/brain/types.d.ts +0 -1
  33. package/dist/brain/types.js +0 -1
  34. package/dist/bridge/pipeline.d.ts +0 -1
  35. package/dist/bridge/pipeline.js +0 -1
  36. package/dist/core/context-builder.d.ts +0 -1
  37. package/dist/core/context-builder.js +0 -1
  38. package/dist/core/gates.d.ts +0 -1
  39. package/dist/core/gates.js +0 -1
  40. package/dist/core/guards.d.ts +3 -2
  41. package/dist/core/guards.js +24 -8
  42. package/dist/core/hooks.d.ts +0 -1
  43. package/dist/core/hooks.js +0 -1
  44. package/dist/core/loop.d.ts +0 -1
  45. package/dist/core/loop.js +5 -2
  46. package/dist/core/types.d.ts +7 -1
  47. package/dist/core/types.js +0 -1
  48. package/dist/definition/parser.d.ts +0 -1
  49. package/dist/definition/parser.js +13 -8
  50. package/dist/index.d.ts +0 -1
  51. package/dist/index.js +0 -1
  52. package/dist/quality/scorer.d.ts +0 -1
  53. package/dist/quality/scorer.js +0 -1
  54. package/dist/server.d.ts +0 -1
  55. package/dist/server.js +0 -1
  56. package/dist/tools/dedup.d.ts +0 -1
  57. package/dist/tools/dedup.js +0 -1
  58. package/dist/tools/executor.d.ts +0 -1
  59. package/dist/tools/executor.js +21 -4
  60. package/package.json +2 -7
  61. package/dist/adapters/index.d.ts.map +0 -1
  62. package/dist/adapters/index.js.map +0 -1
  63. package/dist/adapters/redis-control.d.ts.map +0 -1
  64. package/dist/adapters/redis-control.js.map +0 -1
  65. package/dist/adapters/redis-lock.d.ts.map +0 -1
  66. package/dist/adapters/redis-lock.js.map +0 -1
  67. package/dist/adapters/redis-memory.d.ts.map +0 -1
  68. package/dist/adapters/redis-memory.js.map +0 -1
  69. package/dist/adapters/redis-types.d.ts.map +0 -1
  70. package/dist/adapters/redis-types.js.map +0 -1
  71. package/dist/brain/anthropic.d.ts.map +0 -1
  72. package/dist/brain/anthropic.js.map +0 -1
  73. package/dist/brain/factory.d.ts.map +0 -1
  74. package/dist/brain/factory.js.map +0 -1
  75. package/dist/brain/gemini.d.ts.map +0 -1
  76. package/dist/brain/gemini.js.map +0 -1
  77. package/dist/brain/ollama.d.ts.map +0 -1
  78. package/dist/brain/ollama.js.map +0 -1
  79. package/dist/brain/openai.d.ts.map +0 -1
  80. package/dist/brain/openai.js.map +0 -1
  81. package/dist/brain/pricing.d.ts.map +0 -1
  82. package/dist/brain/pricing.js.map +0 -1
  83. package/dist/brain/retry.d.ts.map +0 -1
  84. package/dist/brain/retry.js.map +0 -1
  85. package/dist/brain/types.d.ts.map +0 -1
  86. package/dist/brain/types.js.map +0 -1
  87. package/dist/bridge/pipeline.d.ts.map +0 -1
  88. package/dist/bridge/pipeline.js.map +0 -1
  89. package/dist/core/context-builder.d.ts.map +0 -1
  90. package/dist/core/context-builder.js.map +0 -1
  91. package/dist/core/gates.d.ts.map +0 -1
  92. package/dist/core/gates.js.map +0 -1
  93. package/dist/core/guards.d.ts.map +0 -1
  94. package/dist/core/guards.js.map +0 -1
  95. package/dist/core/hooks.d.ts.map +0 -1
  96. package/dist/core/hooks.js.map +0 -1
  97. package/dist/core/loop.d.ts.map +0 -1
  98. package/dist/core/loop.js.map +0 -1
  99. package/dist/core/types.d.ts.map +0 -1
  100. package/dist/core/types.js.map +0 -1
  101. package/dist/definition/parser.d.ts.map +0 -1
  102. package/dist/definition/parser.js.map +0 -1
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js.map +0 -1
  105. package/dist/quality/scorer.d.ts.map +0 -1
  106. package/dist/quality/scorer.js.map +0 -1
  107. package/dist/server.d.ts.map +0 -1
  108. package/dist/server.js.map +0 -1
  109. package/dist/tools/dedup.d.ts.map +0 -1
  110. package/dist/tools/dedup.js.map +0 -1
  111. package/dist/tools/executor.d.ts.map +0 -1
  112. 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
  ```
@@ -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
@@ -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
- //# sourceMappingURL=index.js.map
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
+ }
@@ -22,4 +22,3 @@ export declare class RedisControlBus {
22
22
  /** Returns the disabled tool name if the command is disable, else null */
23
23
  static disabledTool(command: string | null): string | null;
24
24
  }
25
- //# sourceMappingURL=redis-control.d.ts.map
@@ -45,4 +45,3 @@ export class RedisControlBus {
45
45
  return command.slice("disabled:".length);
46
46
  }
47
47
  }
48
- //# sourceMappingURL=redis-control.js.map
@@ -28,4 +28,3 @@ export declare class RedisDistributedLock {
28
28
  */
29
29
  acquireWithRetry(sessionId: string, ttlMs: number, waitMs?: number, retryInterval?: number): Promise<LockHandle>;
30
30
  }
31
- //# sourceMappingURL=redis-lock.d.ts.map
@@ -70,4 +70,3 @@ export class RedisDistributedLock {
70
70
  function sleep(ms) {
71
71
  return new Promise((resolve) => setTimeout(resolve, ms));
72
72
  }
73
- //# sourceMappingURL=redis-lock.js.map
@@ -42,4 +42,3 @@ export declare class RedisMemory {
42
42
  }
43
43
  /** Connect to Redis using ioredis (peer dependency) */
44
44
  export declare function connectRedis(url: string): Promise<RedisLike>;
45
- //# sourceMappingURL=redis-memory.d.ts.map
@@ -90,4 +90,3 @@ export async function connectRedis(url) {
90
90
  const Redis = (mod.default ?? mod);
91
91
  return new Redis(url);
92
92
  }
93
- //# sourceMappingURL=redis-memory.js.map
@@ -16,4 +16,3 @@ export interface RedisLike {
16
16
  hset(key: string, field: string, value: string): Promise<unknown>;
17
17
  eval(script: string, numkeys: number, ...args: any[]): Promise<unknown>;
18
18
  }
19
- //# sourceMappingURL=redis-types.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=redis-types.js.map
@@ -7,4 +7,3 @@ export declare function createAnthropicBrain(opts: {
7
7
  apiKey?: string;
8
8
  model?: string;
9
9
  }): Brain;
10
- //# sourceMappingURL=anthropic.d.ts.map
@@ -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
- const { default: Anthropic } = await import("@anthropic-ai/sdk");
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
- messages.push({ role: "user", content: input.raw });
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
- input_schema: {
29
- type: "object",
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
@@ -4,4 +4,3 @@
4
4
  */
5
5
  import type { Brain, AgentDefinition } from "../core/types.js";
6
6
  export declare function buildBrain(def: AgentDefinition): Brain;
7
- //# sourceMappingURL=factory.d.ts.map
@@ -21,4 +21,3 @@ export function buildBrain(def) {
21
21
  throw new Error(`msm-mini: unknown brain provider "${String(provider)}". Pass a Brain instance directly to createAgent().`);
22
22
  }
23
23
  }
24
- //# sourceMappingURL=factory.js.map
@@ -7,4 +7,3 @@ export declare function createGeminiBrain(opts: {
7
7
  apiKey?: string;
8
8
  model?: string;
9
9
  }): Brain;
10
- //# sourceMappingURL=gemini.d.ts.map
@@ -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 { GoogleGenerativeAI } from "@google/generative-ai";
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
- let userText = input.raw;
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
@@ -7,4 +7,3 @@ export declare function createOllamaBrain(opts: {
7
7
  endpoint?: string;
8
8
  model?: string;
9
9
  }): Brain;
10
- //# sourceMappingURL=ollama.d.ts.map
@@ -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
- { role: "user", content: input.raw },
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({ model, messages, stream: false }),
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
- ...(inputTokens !== undefined || outputTokens !== undefined
34
- ? { usage: { ...(inputTokens !== undefined ? { inputTokens } : {}), ...(outputTokens !== undefined ? { outputTokens } : {}) } }
35
- : {}),
69
+ ...usageBlock,
36
70
  };
37
71
  },
38
72
  };
39
73
  }
40
- //# sourceMappingURL=ollama.js.map
@@ -8,4 +8,3 @@ export declare function createOpenAIBrain(opts: {
8
8
  model?: string;
9
9
  baseURL?: string;
10
10
  }): Brain;
11
- //# sourceMappingURL=openai.d.ts.map