@msm-core/mini 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/LICENSE +21 -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 +7 -12
- 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 +4 -9
- 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 +40 -0
- package/dist/brain/tool-context.js +37 -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 +12 -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 +1 -6
- 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/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.
|
|
@@ -27,21 +28,16 @@ export function createAnthropicBrain(opts) {
|
|
|
27
28
|
role: m.role === "assistant" ? "assistant" : "user",
|
|
28
29
|
content: m.content,
|
|
29
30
|
}));
|
|
30
|
-
|
|
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) });
|
|
31
35
|
const tools = input.tools.length > 0
|
|
32
36
|
? input.tools.map((t) => ({
|
|
33
37
|
name: t.name,
|
|
34
38
|
description: t.description,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
properties: Object.fromEntries(Object.entries(t.parameters).map(([k, v]) => [
|
|
38
|
-
k,
|
|
39
|
-
{ type: v.type, description: v.description },
|
|
40
|
-
])),
|
|
41
|
-
required: Object.entries(t.parameters)
|
|
42
|
-
.filter(([, v]) => v.required)
|
|
43
|
-
.map(([k]) => k),
|
|
44
|
-
},
|
|
39
|
+
// toolParamsToJsonSchema keeps `enum` (the inline mapping dropped it).
|
|
40
|
+
input_schema: toolParamsToJsonSchema(t.parameters),
|
|
45
41
|
}))
|
|
46
42
|
: undefined;
|
|
47
43
|
const baseParams = {
|
|
@@ -87,4 +83,3 @@ export function createAnthropicBrain(opts) {
|
|
|
87
83
|
},
|
|
88
84
|
};
|
|
89
85
|
}
|
|
90
|
-
//# 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,6 +3,7 @@
|
|
|
3
3
|
* Peer dependency: @google/generative-ai >= 0.14.0
|
|
4
4
|
*/
|
|
5
5
|
import { withRetry } from "./retry.js";
|
|
6
|
+
import { foldToolResults } from "./tool-context.js";
|
|
6
7
|
export function createGeminiBrain(opts) {
|
|
7
8
|
const model = opts.model ?? "gemini-2.5-flash";
|
|
8
9
|
return {
|
|
@@ -40,6 +41,7 @@ export function createGeminiBrain(opts) {
|
|
|
40
41
|
type: v.type.toUpperCase(),
|
|
41
42
|
description: v.description,
|
|
42
43
|
...(v.enum ? { enum: v.enum } : {}),
|
|
44
|
+
...(v.items ? { items: { type: v.items.type.toUpperCase() } } : {}),
|
|
43
45
|
},
|
|
44
46
|
])),
|
|
45
47
|
required: Object.entries(t.parameters)
|
|
@@ -107,15 +109,8 @@ function buildGeminiContents(input) {
|
|
|
107
109
|
const role = msg.role === "assistant" ? "model" : "user";
|
|
108
110
|
contents.push({ role, parts: [{ text: msg.content }] });
|
|
109
111
|
}
|
|
110
|
-
// Current user message
|
|
111
|
-
|
|
112
|
-
if (input.tool_results?.length) {
|
|
113
|
-
const resultSummary = input.tool_results
|
|
114
|
-
.map((r) => `[${r.tool}] ${r.status}: ${JSON.stringify(r.result ?? r.error)}`)
|
|
115
|
-
.join("\n");
|
|
116
|
-
userText = `Tool results:\n${resultSummary}\n\n${input.raw}`;
|
|
117
|
-
}
|
|
112
|
+
// Current user message — shared fold + finalize nudge (parity with all brains).
|
|
113
|
+
const userText = foldToolResults(input.raw, input.tool_results);
|
|
118
114
|
contents.push({ role: "user", parts: [{ text: userText }] });
|
|
119
115
|
return contents;
|
|
120
116
|
}
|
|
121
|
-
//# 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
package/dist/brain/openai.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 createOpenAIBrain(opts) {
|
|
8
9
|
const model = opts.model ?? "gpt-4o-mini";
|
|
9
10
|
return {
|
|
@@ -14,20 +15,10 @@ export function createOpenAIBrain(opts) {
|
|
|
14
15
|
apiKey: opts.apiKey ?? process.env["OPENAI_API_KEY"],
|
|
15
16
|
...(opts.baseURL ? { baseURL: opts.baseURL } : {}),
|
|
16
17
|
});
|
|
17
|
-
// Fold prior tool results into the current user turn
|
|
18
|
-
// what its tool calls returned
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
let userContent = input.raw;
|
|
22
|
-
if (input.tool_results?.length) {
|
|
23
|
-
const resultSummary = input.tool_results
|
|
24
|
-
.map((r) => `[${r.tool}] ${r.status}: ${JSON.stringify(r.result ?? r.error)}`)
|
|
25
|
-
.join("\n");
|
|
26
|
-
userContent =
|
|
27
|
-
`Tool results so far:\n${resultSummary}\n\n` +
|
|
28
|
-
`If these results are sufficient to answer, respond directly with a final ` +
|
|
29
|
-
`answer and do NOT call any tool again.\n\nUser request: ${input.raw}`;
|
|
30
|
-
}
|
|
18
|
+
// Fold prior tool results into the current user turn (shared with all
|
|
19
|
+
// brains) so the model sees what its tool calls returned and is nudged to
|
|
20
|
+
// finalize rather than re-calling forever.
|
|
21
|
+
const userContent = foldToolResults(input.raw, input.tool_results);
|
|
31
22
|
const messages = [
|
|
32
23
|
{ role: "system", content: input.system_context },
|
|
33
24
|
...input.history.map((m) => ({
|
|
@@ -42,20 +33,7 @@ export function createOpenAIBrain(opts) {
|
|
|
42
33
|
function: {
|
|
43
34
|
name: t.name,
|
|
44
35
|
description: t.description,
|
|
45
|
-
parameters:
|
|
46
|
-
type: "object",
|
|
47
|
-
properties: Object.fromEntries(Object.entries(t.parameters).map(([k, v]) => [
|
|
48
|
-
k,
|
|
49
|
-
{
|
|
50
|
-
type: v.type,
|
|
51
|
-
description: v.description,
|
|
52
|
-
enum: v.enum,
|
|
53
|
-
},
|
|
54
|
-
])),
|
|
55
|
-
required: Object.entries(t.parameters)
|
|
56
|
-
.filter(([, v]) => v.required)
|
|
57
|
-
.map(([k]) => k),
|
|
58
|
-
},
|
|
36
|
+
parameters: toolParamsToJsonSchema(t.parameters),
|
|
59
37
|
},
|
|
60
38
|
}))
|
|
61
39
|
: undefined;
|
|
@@ -112,4 +90,3 @@ export function createOpenAIBrain(opts) {
|
|
|
112
90
|
},
|
|
113
91
|
};
|
|
114
92
|
}
|
|
115
|
-
//# sourceMappingURL=openai.js.map
|
package/dist/brain/pricing.d.ts
CHANGED
package/dist/brain/pricing.js
CHANGED
package/dist/brain/retry.d.ts
CHANGED
|
@@ -18,4 +18,3 @@ export interface RetryOpts {
|
|
|
18
18
|
export declare function isTransientError(err: unknown): boolean;
|
|
19
19
|
/** Run `fn`, retrying once (by default) on transient provider errors. */
|
|
20
20
|
export declare function withRetry<T>(fn: () => Promise<T>, opts?: RetryOpts): Promise<T>;
|
|
21
|
-
//# sourceMappingURL=retry.d.ts.map
|
package/dist/brain/retry.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared brain helpers — the two things EVERY provider must do identically so
|
|
3
|
+
* tool-using agents behave the same across OpenAI / Anthropic / Gemini / Ollama.
|
|
4
|
+
* Previously each brain hand-rolled these (or skipped them), which is why the
|
|
5
|
+
* Anthropic and Ollama paths regressed (audit H1/H2):
|
|
6
|
+
*
|
|
7
|
+
* 1. foldToolResults() — splice the prior iteration's tool results into the
|
|
8
|
+
* current user turn WITH a "don't call again if these suffice" nudge. Without
|
|
9
|
+
* it the model never sees what its tool call returned, re-emits `use_tool`
|
|
10
|
+
* every iteration until the guard cap, and the loop's force-finalize answer is
|
|
11
|
+
* ungrounded. OpenAI/Gemini did this; Anthropic/Ollama did not.
|
|
12
|
+
*
|
|
13
|
+
* 2. toolParamsToJsonSchema() — map a tool's ToolParameter record to a JSON
|
|
14
|
+
* Schema object INCLUDING `enum`. The Anthropic brain dropped `enum`, so an
|
|
15
|
+
* enum-constrained parameter was silently unconstrained there.
|
|
16
|
+
*/
|
|
17
|
+
import type { ToolParameter, ToolResult } from "../core/types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Fold prior-iteration tool results into the user turn. Returns `raw` unchanged
|
|
20
|
+
* when there are none, so it is safe to call on the first iteration.
|
|
21
|
+
*/
|
|
22
|
+
export declare function foldToolResults(raw: string, toolResults?: ToolResult[]): string;
|
|
23
|
+
export interface JsonSchemaObject {
|
|
24
|
+
type: "object";
|
|
25
|
+
properties: Record<string, {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
enum?: string[];
|
|
29
|
+
items?: {
|
|
30
|
+
type: string;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
required: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Canonical JSON-Schema for a tool's parameters. Includes `enum` when present.
|
|
37
|
+
* Providers that need a different casing (e.g. Gemini's UPPERCASE types) adapt
|
|
38
|
+
* from this shape; the property/required/enum semantics stay identical.
|
|
39
|
+
*/
|
|
40
|
+
export declare function toolParamsToJsonSchema(parameters: Record<string, ToolParameter>): JsonSchemaObject;
|