@msm-core/mini 0.9.0 → 0.14.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 +279 -0
- package/dist/adapters/index.d.ts +12 -2
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/memory-control.d.ts +43 -0
- package/dist/adapters/memory-control.js +56 -0
- package/dist/adapters/memory-dedup.d.ts +36 -0
- package/dist/adapters/memory-dedup.js +59 -0
- package/dist/adapters/memory-lock.d.ts +45 -0
- package/dist/adapters/memory-lock.js +89 -0
- package/dist/adapters/redis-control.d.ts +2 -1
- package/dist/adapters/redis-lock.d.ts +9 -5
- package/dist/brain/anthropic.js +8 -1
- package/dist/brain/gemini.js +15 -2
- package/dist/brain/ollama.js +8 -2
- package/dist/brain/openai.js +9 -2
- package/dist/brain/retry.d.ts +98 -2
- package/dist/brain/retry.js +132 -2
- package/dist/brain/streaming.d.ts +42 -0
- package/dist/brain/streaming.js +44 -1
- package/dist/core/hooks.d.ts +14 -2
- package/dist/core/hooks.js +20 -3
- package/dist/core/loop.js +148 -23
- package/dist/core/types.d.ts +349 -4
- package/dist/core/types.js +90 -0
- package/dist/definition/skills.d.ts +74 -0
- package/dist/definition/skills.js +119 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +28 -0
- package/dist/tools/dedup.d.ts +22 -1
- package/dist/tools/dedup.js +28 -0
- package/dist/tools/executor.d.ts +49 -5
- package/dist/tools/executor.js +38 -5
- package/package.json +4 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills — reusable instruction packs an agent loads, "files all the way down."
|
|
3
|
+
*
|
|
4
|
+
* Drop markdown into a folder and each file becomes part of the agent's
|
|
5
|
+
* definition, so the model gains that know-how with no rebuild and no code
|
|
6
|
+
* change. Two shapes are read, and they are the shapes Claude-style skill packs
|
|
7
|
+
* already use:
|
|
8
|
+
*
|
|
9
|
+
* skills/
|
|
10
|
+
* summarizing/SKILL.md ← a folder pack
|
|
11
|
+
* tone-of-voice.md ← a single-file skill
|
|
12
|
+
*
|
|
13
|
+
* `loadSkills(dir)` returns ONE markdown block (`## Skills`), or `""` when
|
|
14
|
+
* there is nothing to load. **Composition stays with the host**: this module
|
|
15
|
+
* reads files and concatenates them; appending the block to a definition is the
|
|
16
|
+
* caller's line of code. The loop is not involved and does not know skills
|
|
17
|
+
* exist — `parseDefinition` has always *stripped* a `## Skills` section as
|
|
18
|
+
* "owned by the app layer", and that stays true.
|
|
19
|
+
*
|
|
20
|
+
* ── Lifted from `nisus/runtime/skills/loader.ts`, hardened where it mattered ──
|
|
21
|
+
*
|
|
22
|
+
* The pattern was right, so it was carried over rather than reinvented. Three
|
|
23
|
+
* things changed, each for a failure mode, and each pinned by a guard in
|
|
24
|
+
* `tests/skills.test.ts` — including a differential test against a verbatim
|
|
25
|
+
* transcription of the original, so any divergence beyond these is a bug:
|
|
26
|
+
*
|
|
27
|
+
* 1. **The order is deterministic.** The original iterated `readdirSync`
|
|
28
|
+
* directly, and that order is filesystem- and platform-dependent (APFS
|
|
29
|
+
* hands back roughly hash order; ext4 differs again). Two machines with
|
|
30
|
+
* the same two skills therefore built two different definitions — the same
|
|
31
|
+
* agent with two fingerprints, which is the enemy of replay
|
|
32
|
+
* (`@msm-core/replay` fingerprints `system_context`) and of any diff a
|
|
33
|
+
* human tries to read. Entries are now sorted by code unit before they are
|
|
34
|
+
* read. **Not** `localeCompare`: that depends on ICU data and the ambient
|
|
35
|
+
* locale, which would reintroduce exactly the nondeterminism being
|
|
36
|
+
* removed.
|
|
37
|
+
*
|
|
38
|
+
* 2. **Diagnostics go to an injected port, never to `console`.** Same reason
|
|
39
|
+
* as `@msm-core/mcp`'s `McpLogPort`: a library that prints is a library you
|
|
40
|
+
* cannot embed. Absent a port, the loader is silent.
|
|
41
|
+
*
|
|
42
|
+
* 3. **An empty skill file contributes nothing instead of a bare separator.**
|
|
43
|
+
* The original pushed `readFileSync(...).trim()` unconditionally, so an
|
|
44
|
+
* empty (or whitespace-only) `.md` injected `"\n\n---\n\n\n\n---\n\n"`
|
|
45
|
+
* into the text the model reads — a horizontal rule with no section under
|
|
46
|
+
* it, mid-definition. It is now skipped, with a warning naming the file,
|
|
47
|
+
* because a file someone left empty is a mistake worth hearing about.
|
|
48
|
+
*
|
|
49
|
+
* What deliberately did NOT change: a folder without a `SKILL.md` is skipped in
|
|
50
|
+
* silence (it is a plain folder, not a failure); a non-`.md` file is ignored;
|
|
51
|
+
* an entry that throws is reported and its siblings still load; and a folder
|
|
52
|
+
* pack and a single-file skill sitting side by side are BOTH read — the folder
|
|
53
|
+
* from its `SKILL.md`, the file from itself. That last one is the precedence
|
|
54
|
+
* rule, and it is a rule rather than an accident because a guard says so.
|
|
55
|
+
*/
|
|
56
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
57
|
+
import { extname, join } from "node:path";
|
|
58
|
+
/** The pack marker inside a skill folder. */
|
|
59
|
+
const PACK_FILE = "SKILL.md";
|
|
60
|
+
/** The heading the block is published under. */
|
|
61
|
+
const BLOCK_HEADING = "## Skills";
|
|
62
|
+
/** What sits between two skills inside the block. */
|
|
63
|
+
const SEPARATOR = "\n\n---\n\n";
|
|
64
|
+
/**
|
|
65
|
+
* Code-unit order: total, locale-independent, and identical on every platform.
|
|
66
|
+
* `Array.prototype.sort()`'s default is also code-unit order, but it gets there
|
|
67
|
+
* by stringifying — spelling it out states the intent that the guard tests.
|
|
68
|
+
*/
|
|
69
|
+
function byCodeUnit(a, b) {
|
|
70
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read every skill under `dir` and return them as one markdown block, or `""`
|
|
74
|
+
* when the directory is missing, is not a directory, or holds no skills.
|
|
75
|
+
*
|
|
76
|
+
* Never throws for a skill it cannot read: the failure is reported on `log` and
|
|
77
|
+
* the remaining skills still load. A definition missing one section is worth
|
|
78
|
+
* more than an agent that will not start.
|
|
79
|
+
*/
|
|
80
|
+
export function loadSkills(dir, log = {}) {
|
|
81
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
82
|
+
return "";
|
|
83
|
+
// Sorted BEFORE anything is read: the order of this array is the order of the
|
|
84
|
+
// block, and the block is part of what the model is asked.
|
|
85
|
+
const entries = [...readdirSync(dir)].sort(byCodeUnit);
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const path = join(dir, entry);
|
|
89
|
+
let text;
|
|
90
|
+
try {
|
|
91
|
+
const stat = statSync(path);
|
|
92
|
+
if (stat.isDirectory()) {
|
|
93
|
+
const pack = join(path, PACK_FILE);
|
|
94
|
+
if (!existsSync(pack))
|
|
95
|
+
continue; // a plain folder, not a skill pack
|
|
96
|
+
text = readFileSync(pack, "utf-8").trim();
|
|
97
|
+
}
|
|
98
|
+
else if (extname(entry).toLowerCase() === ".md") {
|
|
99
|
+
text = readFileSync(path, "utf-8").trim();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
continue; // not a skill in either shape
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
log.warn?.(`[skills] failed to load ${entry}: ${err.message}`);
|
|
107
|
+
continue; // one unreadable skill must not cost the others
|
|
108
|
+
}
|
|
109
|
+
if (text === "") {
|
|
110
|
+
log.warn?.(`[skills] skipped empty "${entry}"`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
parts.push(text);
|
|
114
|
+
log.info?.(`[skills] loaded "${entry}"`);
|
|
115
|
+
}
|
|
116
|
+
if (parts.length === 0)
|
|
117
|
+
return "";
|
|
118
|
+
return `${BLOCK_HEADING}\n\n${parts.join(SEPARATOR)}`;
|
|
119
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,10 +15,14 @@ export { createAnthropicBrain } from "./brain/anthropic.js";
|
|
|
15
15
|
export { createOllamaBrain } from "./brain/ollama.js";
|
|
16
16
|
export { buildBrain } from "./brain/factory.js";
|
|
17
17
|
export { parseDefinition } from "./definition/parser.js";
|
|
18
|
+
export { loadSkills } from "./definition/skills.js";
|
|
19
|
+
export type { SkillsLogPort } from "./definition/skills.js";
|
|
18
20
|
export { createDelegateTool, DELEGATE_TOOL_NAME } from "./tools/delegate.js";
|
|
19
21
|
export type { DelegateTool, DelegateToolOptions } from "./tools/delegate.js";
|
|
20
|
-
export type { Agent, AgentConfig, AgentEvent, AgentContext, AgentDefinition, AgentHooks, BeforeToolHookResult, Brain, BrainChunk, BrainOrchestration, BrainPayload, BrainRunInput, BrainToolCall, ChunkInfo, CompactionDecision, CompactionPort, DocumentState, GateConfig, GuardConfig, GuardSignal, GuardSignalType, IterationInfo, LoopOutcome, MemoryEntry, Message, OutcomeType, OutputValidation, OutputValidator, RedisConfig, RunState, SectionInfo, SessionMetadata, SessionStore, TenantContext, Tool, ToolCallInfo, ToolDefinition, ToolMeta, ToolParameter, ToolResult, } from "./core/types.js";
|
|
22
|
+
export type { Agent, AgentConfig, AgentEvent, AgentContext, AgentDefinition, AgentHooks, BeforeToolHookResult, Brain, BrainChunk, BrainOrchestration, BrainPayload, BrainRunInput, BrainToolCall, ChunkInfo, CompactionDecision, CompactionPort, ControlBusPort, DedupPort, DocumentState, GateConfig, GuardConfig, GuardSignal, GuardSignalType, IterationInfo, LockHandle, LoopOutcome, MemoryEntry, Message, OutcomeType, OutputValidation, OutputValidator, RedisConfig, ResetAwareChunkHook, ResetAwareChunkSink, RunLockPort, RunState, SectionInfo, SessionMetadata, SessionStore, TenantContext, Tool, ToolCallInfo, ToolDefinition, ToolMeta, ToolParameter, ToolResult, } from "./core/types.js";
|
|
21
23
|
export type { ContextBudget } from "./core/context-builder.js";
|
|
24
|
+
export { acceptResets, isResetAware, acceptChunkResets, isResetAwareHook, } from "./core/types.js";
|
|
25
|
+
export { respondingModel } from "./core/types.js";
|
|
22
26
|
export { resolveGuards, DEFAULT_GUARDS } from "./core/guards.js";
|
|
23
27
|
export { scoreOutcome } from "./quality/scorer.js";
|
|
24
28
|
export type { QualityScore, QualityFlag } from "./quality/scorer.js";
|
package/dist/index.js
CHANGED
|
@@ -18,8 +18,36 @@ export { createOllamaBrain } from "./brain/ollama.js";
|
|
|
18
18
|
export { buildBrain } from "./brain/factory.js";
|
|
19
19
|
// ── Definition parser ───────────────────────────────────────────────────────
|
|
20
20
|
export { parseDefinition } from "./definition/parser.js";
|
|
21
|
+
// ── Skills (م١) — instruction packs read from disk, composed by the host ─────
|
|
22
|
+
//
|
|
23
|
+
// `loadSkills(dir)` returns one `## Skills` markdown block; appending it to a
|
|
24
|
+
// definition is the composition root's line of code, not the loop's.
|
|
25
|
+
export { loadSkills } from "./definition/skills.js";
|
|
21
26
|
// ── Delegation (ر٢) — one agent asks another, as an ordinary tool ───────────
|
|
22
27
|
export { createDelegateTool, DELEGATE_TOOL_NAME } from "./tools/delegate.js";
|
|
28
|
+
// ── Streaming: opting in to a reset (ص٢/٢ · ص٣/٢) ───────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// Two doors onto the same opt-in, one per altitude.
|
|
31
|
+
//
|
|
32
|
+
// • A consumer that drives a brain DIRECTLY wraps its chunk sink in
|
|
33
|
+
// `acceptResets` (ص٢/٢) and stops losing the tail of a retried answer.
|
|
34
|
+
// • A consumer that runs an AGENT wraps its `AgentHooks.onChunk` in
|
|
35
|
+
// `acceptChunkResets` (ص٣/٢), and the loop declares to the brain on its
|
|
36
|
+
// behalf; `ChunkInfo.reset` then arrives on the first chunk of a retried
|
|
37
|
+
// attempt. This is the door most people want — `onChunk` is where an SSE
|
|
38
|
+
// channel is actually wired.
|
|
39
|
+
//
|
|
40
|
+
// Without a wrapper nothing changes anywhere: no reset is produced, and the
|
|
41
|
+
// object delivered to `onChunk` keeps the exact three keys it has always had.
|
|
42
|
+
export { acceptResets, isResetAware, acceptChunkResets, isResetAwareHook, } from "./core/types.js";
|
|
43
|
+
// ── Who answered (س٦/٤) ─────────────────────────────────────────────────────
|
|
44
|
+
//
|
|
45
|
+
// The reading the four bundled brains use to fill `BrainPayload.model` from
|
|
46
|
+
// their provider's reply — exported so a custom brain fills it the same way
|
|
47
|
+
// instead of inventing a second reading that drifts. Hand it whatever the
|
|
48
|
+
// provider returned; it yields `{ model }` when the reply named one and `{}`
|
|
49
|
+
// when it did not, ready to spread into the payload.
|
|
50
|
+
export { respondingModel } from "./core/types.js";
|
|
23
51
|
// ── Guard utilities ─────────────────────────────────────────────────────────
|
|
24
52
|
export { resolveGuards, DEFAULT_GUARDS } from "./core/guards.js";
|
|
25
53
|
// ── Quality scorer ──────────────────────────────────────────────────────────
|
package/dist/tools/dedup.d.ts
CHANGED
|
@@ -5,12 +5,33 @@
|
|
|
5
5
|
* Stored in Redis as: {prefix}:session:{id}:tools:dedup hash field
|
|
6
6
|
* TTL: 5 minutes (configurable)
|
|
7
7
|
*/
|
|
8
|
-
import type { ToolResult } from "../core/types.js";
|
|
8
|
+
import type { DedupPort, ToolResult } from "../core/types.js";
|
|
9
9
|
type RedisLike = {
|
|
10
10
|
hget(key: string, field: string): Promise<string | null>;
|
|
11
11
|
hset(key: string, field: string, value: string): Promise<unknown>;
|
|
12
12
|
expire(key: string, seconds: number): Promise<unknown>;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* The bundled Redis-backed `DedupPort` — the three functions below, behind the
|
|
16
|
+
* port the executor now takes.
|
|
17
|
+
*
|
|
18
|
+
* It adds nothing: `check`/`store` are `checkDedup`/`storeDedup` with the key
|
|
19
|
+
* built by `toolDedupKey` from the prefix this was constructed with, which is
|
|
20
|
+
* precisely what the executor used to do inline with a raw client. The class
|
|
21
|
+
* exists so that the executor talks to ONE thing whether the dedup came from a
|
|
22
|
+
* config or from Redis, and so the Redis path keeps its exact key shape and TTL
|
|
23
|
+
* behaviour while doing so.
|
|
24
|
+
*
|
|
25
|
+
* The prefix is the tenant-scoped one when the loop builds this
|
|
26
|
+
* (`{prefix}:{companyId}:{agentType}`); an injected port scopes itself.
|
|
27
|
+
*/
|
|
28
|
+
export declare class RedisToolDedup implements DedupPort {
|
|
29
|
+
private readonly redis;
|
|
30
|
+
private readonly prefix;
|
|
31
|
+
constructor(redis: RedisLike, prefix: string);
|
|
32
|
+
check(sessionId: string, hash: string): Promise<ToolResult | null>;
|
|
33
|
+
store(sessionId: string, hash: string, result: ToolResult, ttlSeconds: number): Promise<void>;
|
|
34
|
+
}
|
|
14
35
|
export declare function toolDedupKey(prefix: string, sessionId: string): string;
|
|
15
36
|
export declare function checkDedup(redis: RedisLike, dedupKey: string, hash: string): Promise<ToolResult | null>;
|
|
16
37
|
export declare function storeDedup(redis: RedisLike, dedupKey: string, hash: string, result: ToolResult, ttlSeconds: number): Promise<void>;
|
package/dist/tools/dedup.js
CHANGED
|
@@ -6,6 +6,34 @@
|
|
|
6
6
|
* TTL: 5 minutes (configurable)
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
/**
|
|
10
|
+
* The bundled Redis-backed `DedupPort` — the three functions below, behind the
|
|
11
|
+
* port the executor now takes.
|
|
12
|
+
*
|
|
13
|
+
* It adds nothing: `check`/`store` are `checkDedup`/`storeDedup` with the key
|
|
14
|
+
* built by `toolDedupKey` from the prefix this was constructed with, which is
|
|
15
|
+
* precisely what the executor used to do inline with a raw client. The class
|
|
16
|
+
* exists so that the executor talks to ONE thing whether the dedup came from a
|
|
17
|
+
* config or from Redis, and so the Redis path keeps its exact key shape and TTL
|
|
18
|
+
* behaviour while doing so.
|
|
19
|
+
*
|
|
20
|
+
* The prefix is the tenant-scoped one when the loop builds this
|
|
21
|
+
* (`{prefix}:{companyId}:{agentType}`); an injected port scopes itself.
|
|
22
|
+
*/
|
|
23
|
+
export class RedisToolDedup {
|
|
24
|
+
redis;
|
|
25
|
+
prefix;
|
|
26
|
+
constructor(redis, prefix) {
|
|
27
|
+
this.redis = redis;
|
|
28
|
+
this.prefix = prefix;
|
|
29
|
+
}
|
|
30
|
+
async check(sessionId, hash) {
|
|
31
|
+
return checkDedup(this.redis, toolDedupKey(this.prefix, sessionId), hash);
|
|
32
|
+
}
|
|
33
|
+
async store(sessionId, hash, result, ttlSeconds) {
|
|
34
|
+
await storeDedup(this.redis, toolDedupKey(this.prefix, sessionId), hash, result, ttlSeconds);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
9
37
|
export function toolDedupKey(prefix, sessionId) {
|
|
10
38
|
return `${prefix}:session:${sessionId}:tools:dedup`;
|
|
11
39
|
}
|
package/dist/tools/executor.d.ts
CHANGED
|
@@ -10,26 +10,70 @@
|
|
|
10
10
|
* 4. Execute — call tool.execute()
|
|
11
11
|
* 5. Cache — store result in dedup hash
|
|
12
12
|
*/
|
|
13
|
-
import type { Tool, ToolResult, ToolMeta, AgentHooks } from "../core/types.js";
|
|
13
|
+
import type { Tool, ToolResult, ToolMeta, AgentHooks, DedupPort } from "../core/types.js";
|
|
14
14
|
type RedisLike = {
|
|
15
15
|
hget(key: string, field: string): Promise<string | null>;
|
|
16
16
|
hset(key: string, field: string, value: string): Promise<unknown>;
|
|
17
17
|
expire(key: string, seconds: number): Promise<unknown>;
|
|
18
18
|
get(key: string): Promise<string | null>;
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Where step 3's cache lives — a port, or a Redis client to build one from.
|
|
22
|
+
*
|
|
23
|
+
* A union rather than "a port and an optional client", so the compiler still
|
|
24
|
+
* insists on exactly one answer. The second arm is the call this function has
|
|
25
|
+
* always taken (`{ redis, redisPrefix, … }`) and it keeps working character for
|
|
26
|
+
* character; the loop passes the first, because since س٦ it resolves the dedup
|
|
27
|
+
* port itself — injected or Redis-backed — and hands one thing down.
|
|
28
|
+
*/
|
|
29
|
+
type DedupSource = {
|
|
30
|
+
dedup: DedupPort;
|
|
31
|
+
redis?: never;
|
|
32
|
+
redisPrefix?: never;
|
|
33
|
+
} | {
|
|
34
|
+
dedup?: never;
|
|
21
35
|
redis: RedisLike;
|
|
22
36
|
redisPrefix: string;
|
|
37
|
+
};
|
|
38
|
+
export type ExecutorOptions = DedupSource & {
|
|
23
39
|
dedupTtlSeconds: number;
|
|
24
40
|
/** Optional hooks — only onBeforeTool is used at this layer */
|
|
25
41
|
hooks?: Pick<AgentHooks, "onBeforeTool">;
|
|
26
|
-
}
|
|
42
|
+
};
|
|
27
43
|
export interface ExecutionResult {
|
|
28
44
|
result: ToolResult;
|
|
29
45
|
cached: boolean;
|
|
30
46
|
durationMs: number;
|
|
31
47
|
}
|
|
32
48
|
export declare function executeTool(tool: Tool, params: Record<string, unknown>, meta: ToolMeta, opts: ExecutorOptions): Promise<ExecutionResult>;
|
|
33
|
-
/**
|
|
34
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Build a ToolDefinition list from app-provided Tool objects (for brain prompt).
|
|
51
|
+
*
|
|
52
|
+
* **The stamp travels (ر١/١).** `ToolDefinition` has declared `destructive` and
|
|
53
|
+
* `category` since before MCP existed and nothing ever filled them: a tool an
|
|
54
|
+
* operator stamped `destructive` at the composition root reached the model
|
|
55
|
+
* looking exactly like a read-only one. This was never a safety hole — the
|
|
56
|
+
* executor enforces `requiresApproval` above regardless of what the model was
|
|
57
|
+
* told — but a model that is never shown which of its tools change the world
|
|
58
|
+
* cannot be asked to be careful with them, and the two fields existed on the
|
|
59
|
+
* type precisely so that it could be.
|
|
60
|
+
*
|
|
61
|
+
* **Why the parameter widens instead of the body casting.** Mini's `Tool` does
|
|
62
|
+
* not declare the two fields; `McpTool` (ر١) and `DelegateTool` (ر٢) each add
|
|
63
|
+
* them to a plain `Tool`, and both reach here through `AgentConfig.tools:
|
|
64
|
+
* Tool[]` — on the object, off the type. Saying so in the signature keeps it
|
|
65
|
+
* honest: a plain `Tool[]` still satisfies it (both fields are optional), a
|
|
66
|
+
* stamped array is read with no `as` anywhere, and a caller who writes
|
|
67
|
+
* `destructive: "yes"` is told by the compiler instead of dropped in silence.
|
|
68
|
+
*
|
|
69
|
+
* **Absent stays absent.** The conditional spreads are not style: under
|
|
70
|
+
* `exactOptionalPropertyTypes` an unstamped tool must produce the object it
|
|
71
|
+
* produced yesterday, key for key. That is what keeps an unstamped run's
|
|
72
|
+
* `@msm-core/replay` fingerprint unmoved — only a tool that actually carries a
|
|
73
|
+
* stamp changes what the model is asked, and only its fingerprint moves.
|
|
74
|
+
*/
|
|
75
|
+
export declare function toToolDefinitions(tools: ReadonlyArray<Tool & {
|
|
76
|
+
destructive?: boolean;
|
|
77
|
+
category?: string;
|
|
78
|
+
}>): import("../core/types.js").ToolDefinition[];
|
|
35
79
|
export {};
|
package/dist/tools/executor.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 4. Execute — call tool.execute()
|
|
11
11
|
* 5. Cache — store result in dedup hash
|
|
12
12
|
*/
|
|
13
|
-
import { hashToolCall,
|
|
13
|
+
import { hashToolCall, RedisToolDedup } from "./dedup.js";
|
|
14
14
|
export async function executeTool(tool, params, meta, opts) {
|
|
15
15
|
const start = Date.now();
|
|
16
16
|
// Step 1: Validate required parameters
|
|
@@ -95,9 +95,15 @@ export async function executeTool(tool, params, meta, opts) {
|
|
|
95
95
|
}
|
|
96
96
|
// Step 3: Dedup check (keyed on the EFFECTIVE params, so edited-approval runs
|
|
97
97
|
// are not served a cached result for the original params).
|
|
98
|
+
//
|
|
99
|
+
// The hash stays here and stays pure — it is not I/O and there is nothing to
|
|
100
|
+
// swap about it, so every implementation of the port dedups on exactly the
|
|
101
|
+
// same key the Redis one always did.
|
|
102
|
+
const dedup = opts.dedup
|
|
103
|
+
? opts.dedup
|
|
104
|
+
: new RedisToolDedup(opts.redis, opts.redisPrefix);
|
|
98
105
|
const hash = hashToolCall(tool.name, effectiveParams);
|
|
99
|
-
const
|
|
100
|
-
const cached = await checkDedup(opts.redis, dedupKey, hash);
|
|
106
|
+
const cached = await dedup.check(meta.sessionId, hash);
|
|
101
107
|
if (cached) {
|
|
102
108
|
return { result: cached, cached: true, durationMs: Date.now() - start };
|
|
103
109
|
}
|
|
@@ -115,7 +121,7 @@ export async function executeTool(tool, params, meta, opts) {
|
|
|
115
121
|
}
|
|
116
122
|
// Step 5: Cache successful results only
|
|
117
123
|
if (result.status === "ok") {
|
|
118
|
-
await
|
|
124
|
+
await dedup.store(meta.sessionId, hash, result, opts.dedupTtlSeconds);
|
|
119
125
|
}
|
|
120
126
|
return { result, cached: false, durationMs: Date.now() - start };
|
|
121
127
|
}
|
|
@@ -135,7 +141,32 @@ function validateParams(tool, params) {
|
|
|
135
141
|
}
|
|
136
142
|
return null;
|
|
137
143
|
}
|
|
138
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Build a ToolDefinition list from app-provided Tool objects (for brain prompt).
|
|
146
|
+
*
|
|
147
|
+
* **The stamp travels (ر١/١).** `ToolDefinition` has declared `destructive` and
|
|
148
|
+
* `category` since before MCP existed and nothing ever filled them: a tool an
|
|
149
|
+
* operator stamped `destructive` at the composition root reached the model
|
|
150
|
+
* looking exactly like a read-only one. This was never a safety hole — the
|
|
151
|
+
* executor enforces `requiresApproval` above regardless of what the model was
|
|
152
|
+
* told — but a model that is never shown which of its tools change the world
|
|
153
|
+
* cannot be asked to be careful with them, and the two fields existed on the
|
|
154
|
+
* type precisely so that it could be.
|
|
155
|
+
*
|
|
156
|
+
* **Why the parameter widens instead of the body casting.** Mini's `Tool` does
|
|
157
|
+
* not declare the two fields; `McpTool` (ر١) and `DelegateTool` (ر٢) each add
|
|
158
|
+
* them to a plain `Tool`, and both reach here through `AgentConfig.tools:
|
|
159
|
+
* Tool[]` — on the object, off the type. Saying so in the signature keeps it
|
|
160
|
+
* honest: a plain `Tool[]` still satisfies it (both fields are optional), a
|
|
161
|
+
* stamped array is read with no `as` anywhere, and a caller who writes
|
|
162
|
+
* `destructive: "yes"` is told by the compiler instead of dropped in silence.
|
|
163
|
+
*
|
|
164
|
+
* **Absent stays absent.** The conditional spreads are not style: under
|
|
165
|
+
* `exactOptionalPropertyTypes` an unstamped tool must produce the object it
|
|
166
|
+
* produced yesterday, key for key. That is what keeps an unstamped run's
|
|
167
|
+
* `@msm-core/replay` fingerprint unmoved — only a tool that actually carries a
|
|
168
|
+
* stamp changes what the model is asked, and only its fingerprint moves.
|
|
169
|
+
*/
|
|
139
170
|
export function toToolDefinitions(tools) {
|
|
140
171
|
return tools.map((t) => ({
|
|
141
172
|
name: t.name,
|
|
@@ -144,5 +175,7 @@ export function toToolDefinitions(tools) {
|
|
|
144
175
|
...(t.requiresApproval !== undefined
|
|
145
176
|
? { requiresApproval: t.requiresApproval }
|
|
146
177
|
: {}),
|
|
178
|
+
...(t.destructive !== undefined ? { destructive: t.destructive } : {}),
|
|
179
|
+
...(t.category !== undefined ? { category: t.category } : {}),
|
|
147
180
|
}));
|
|
148
181
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msm-core/mini",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Portable AI agent execution loop — brain-agnostic, zero embedded databases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"ioredis": "^5.3.2",
|
|
52
|
-
"@msm-core/session": "^0.
|
|
52
|
+
"@msm-core/session": "^0.3.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^20.0.0",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"license": "UNLICENSED",
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsc",
|
|
71
|
-
"
|
|
71
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
72
|
+
"test": "tsc -p tsconfig.test.json && vitest run",
|
|
72
73
|
"test:watch": "vitest",
|
|
73
74
|
"clean": "rm -rf dist"
|
|
74
75
|
}
|