@msm-core/mini 0.1.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 (94) hide show
  1. package/README.md +297 -0
  2. package/dist/adapters/index.d.ts +9 -0
  3. package/dist/adapters/index.d.ts.map +1 -0
  4. package/dist/adapters/index.js +7 -0
  5. package/dist/adapters/index.js.map +1 -0
  6. package/dist/adapters/redis-control.d.ts +25 -0
  7. package/dist/adapters/redis-control.d.ts.map +1 -0
  8. package/dist/adapters/redis-control.js +48 -0
  9. package/dist/adapters/redis-control.js.map +1 -0
  10. package/dist/adapters/redis-lock.d.ts +31 -0
  11. package/dist/adapters/redis-lock.d.ts.map +1 -0
  12. package/dist/adapters/redis-lock.js +73 -0
  13. package/dist/adapters/redis-lock.js.map +1 -0
  14. package/dist/adapters/redis-memory.d.ts +42 -0
  15. package/dist/adapters/redis-memory.d.ts.map +1 -0
  16. package/dist/adapters/redis-memory.js +87 -0
  17. package/dist/adapters/redis-memory.js.map +1 -0
  18. package/dist/adapters/redis-types.d.ts +17 -0
  19. package/dist/adapters/redis-types.d.ts.map +1 -0
  20. package/dist/adapters/redis-types.js +2 -0
  21. package/dist/adapters/redis-types.js.map +1 -0
  22. package/dist/brain/anthropic.d.ts +10 -0
  23. package/dist/brain/anthropic.d.ts.map +1 -0
  24. package/dist/brain/anthropic.js +73 -0
  25. package/dist/brain/anthropic.js.map +1 -0
  26. package/dist/brain/factory.d.ts +7 -0
  27. package/dist/brain/factory.d.ts.map +1 -0
  28. package/dist/brain/factory.js +24 -0
  29. package/dist/brain/factory.js.map +1 -0
  30. package/dist/brain/gemini.d.ts +10 -0
  31. package/dist/brain/gemini.d.ts.map +1 -0
  32. package/dist/brain/gemini.js +113 -0
  33. package/dist/brain/gemini.js.map +1 -0
  34. package/dist/brain/ollama.d.ts +10 -0
  35. package/dist/brain/ollama.d.ts.map +1 -0
  36. package/dist/brain/ollama.js +38 -0
  37. package/dist/brain/ollama.js.map +1 -0
  38. package/dist/brain/openai.d.ts +11 -0
  39. package/dist/brain/openai.d.ts.map +1 -0
  40. package/dist/brain/openai.js +96 -0
  41. package/dist/brain/openai.js.map +1 -0
  42. package/dist/brain/types.d.ts +6 -0
  43. package/dist/brain/types.d.ts.map +1 -0
  44. package/dist/brain/types.js +2 -0
  45. package/dist/brain/types.js.map +1 -0
  46. package/dist/core/context-builder.d.ts +25 -0
  47. package/dist/core/context-builder.d.ts.map +1 -0
  48. package/dist/core/context-builder.js +107 -0
  49. package/dist/core/context-builder.js.map +1 -0
  50. package/dist/core/gates.d.ts +16 -0
  51. package/dist/core/gates.d.ts.map +1 -0
  52. package/dist/core/gates.js +70 -0
  53. package/dist/core/gates.js.map +1 -0
  54. package/dist/core/guards.d.ts +22 -0
  55. package/dist/core/guards.d.ts.map +1 -0
  56. package/dist/core/guards.js +119 -0
  57. package/dist/core/guards.js.map +1 -0
  58. package/dist/core/hooks.d.ts +13 -0
  59. package/dist/core/hooks.d.ts.map +1 -0
  60. package/dist/core/hooks.js +61 -0
  61. package/dist/core/hooks.js.map +1 -0
  62. package/dist/core/loop.d.ts +16 -0
  63. package/dist/core/loop.d.ts.map +1 -0
  64. package/dist/core/loop.js +255 -0
  65. package/dist/core/loop.js.map +1 -0
  66. package/dist/core/types.d.ts +310 -0
  67. package/dist/core/types.d.ts.map +1 -0
  68. package/dist/core/types.js +8 -0
  69. package/dist/core/types.js.map +1 -0
  70. package/dist/definition/parser.d.ts +12 -0
  71. package/dist/definition/parser.d.ts.map +1 -0
  72. package/dist/definition/parser.js +117 -0
  73. package/dist/definition/parser.js.map +1 -0
  74. package/dist/index.d.ts +22 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +25 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/quality/scorer.d.ts +18 -0
  79. package/dist/quality/scorer.d.ts.map +1 -0
  80. package/dist/quality/scorer.js +22 -0
  81. package/dist/quality/scorer.js.map +1 -0
  82. package/dist/server.d.ts +27 -0
  83. package/dist/server.d.ts.map +1 -0
  84. package/dist/server.js +118 -0
  85. package/dist/server.js.map +1 -0
  86. package/dist/tools/dedup.d.ts +20 -0
  87. package/dist/tools/dedup.d.ts.map +1 -0
  88. package/dist/tools/dedup.js +47 -0
  89. package/dist/tools/dedup.js.map +1 -0
  90. package/dist/tools/executor.d.ts +32 -0
  91. package/dist/tools/executor.d.ts.map +1 -0
  92. package/dist/tools/executor.js +75 -0
  93. package/dist/tools/executor.js.map +1 -0
  94. package/package.json +67 -0
package/README.md ADDED
@@ -0,0 +1,297 @@
1
+ # @msm-core/mini
2
+
3
+ > Portable AI agent execution loop — brain-agnostic, stateless, zero embedded databases.
4
+
5
+ `@msm-core/mini` is the execution engine extracted from [`msm-agent`](https://github.com/msm-core/agent). It owns only the **loop** — brain calls, tool dispatch, guards, and session state in Redis. Everything else (memory, RAG, channels) belongs to your application.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @msm-core/mini
13
+ # peer deps — install only the ones you use:
14
+ npm install openai # OpenAI / compatible (Gemini, etc.)
15
+ npm install @anthropic-ai/sdk # Anthropic
16
+ npm install @google/generative-ai # Google Gemini (native)
17
+ npm install ioredis # required at runtime
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import {
26
+ createAgent,
27
+ createGeminiBrain,
28
+ parseDefinition,
29
+ } from "@msm-core/mini";
30
+
31
+ const brain = createGeminiBrain({ model: "gemini-2.5-flash" });
32
+
33
+ const agent = createAgent({
34
+ definition: "./agent.md", // or a parsed AgentDefinition object
35
+ brain,
36
+ redis: { url: process.env.REDIS_URL! },
37
+ tools: [
38
+ {
39
+ name: "search_kb",
40
+ description: "Search the knowledge base",
41
+ parameters: {
42
+ query: { type: "string", description: "Search query", required: true },
43
+ },
44
+ async execute(args) {
45
+ const results = await myKb.search(String(args.query));
46
+ return { status: "ok", result: { results } };
47
+ },
48
+ },
49
+ ],
50
+ });
51
+
52
+ const outcome = await agent.handle({
53
+ sessionId: "user-123",
54
+ message: "What is the market size for solar in Egypt?",
55
+ context: {
56
+ memories: assembledContext, // from @msm-core/context
57
+ userProfile: { country: "EG", language: "ar" },
58
+ },
59
+ });
60
+
61
+ console.log(outcome.text);
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Agent Definition File
67
+
68
+ Create a Markdown file (`agent.md`) that the parser reads at startup:
69
+
70
+ ```markdown
71
+ # Feasibility Analyst
72
+
73
+ domain: feasibility
74
+ language: ar
75
+
76
+ ## Persona
77
+
78
+ name: Jarvis
79
+ style: professional, concise
80
+
81
+ ## Brain
82
+
83
+ provider: gemini
84
+ model: gemini-2.5-flash
85
+
86
+ ## Capabilities
87
+
88
+ - Conduct investment feasibility studies
89
+ - Analyze financial projections
90
+ - Retrieve and synthesize research documents
91
+
92
+ ## Limits
93
+
94
+ maxIterations: 40
95
+ costCapPerTask: 1.50
96
+ timeoutMs: 600000
97
+
98
+ ## Sections
99
+
100
+ - Executive Summary
101
+ - Market Analysis
102
+ - Financial Projections
103
+ - Risk Assessment
104
+ - Recommendations
105
+ ```
106
+
107
+ Supported providers: `openai` | `anthropic` | `gemini` | `ollama`
108
+
109
+ ---
110
+
111
+ ## Context Injection
112
+
113
+ The agent never queries external stores — your app assembles context before each call and passes it in `AgentEvent.context`. Use [`@msm-core/context`](../context/README.md) to do this automatically.
114
+
115
+ ```typescript
116
+ import { createContextAssembler } from "@msm-core/context";
117
+ import { QdrantAdapter } from "@msm-core/context/adapters";
118
+
119
+ const assembler = createContextAssembler({
120
+ tiers: [{ name: "kb", priority: 1, adapters: [qdrantAdapter] }],
121
+ budget: { maxTokens: 6000 },
122
+ });
123
+
124
+ // In your request handler:
125
+ const ctx = await assembler.build({ text: userMessage, sessionId });
126
+ const outcome = await agent.handle({
127
+ sessionId,
128
+ message: userMessage,
129
+ context: {
130
+ memories: ctx.results.map((r) => ({
131
+ source: r.source,
132
+ content: r.content,
133
+ score: r.score,
134
+ })),
135
+ },
136
+ });
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Brains
142
+
143
+ | Factory | Provider | Env var |
144
+ | ---------------------------- | --------------------------- | ------------------- |
145
+ | `createGeminiBrain(opts)` | Google Gemini | `GEMINI_API_KEY` |
146
+ | `createOpenAIBrain(opts)` | OpenAI / compatible | `OPENAI_API_KEY` |
147
+ | `createAnthropicBrain(opts)` | Anthropic Claude | `ANTHROPIC_API_KEY` |
148
+ | `createOllamaBrain(opts)` | Ollama (local) | — |
149
+ | `buildBrain(def)` | Auto-select from definition | — |
150
+
151
+ `buildBrain(definition)` reads the `## Brain` section of an `AgentDefinition` and returns the correct brain automatically.
152
+
153
+ ---
154
+
155
+ ## HTTP Server
156
+
157
+ Wrap any agent with a minimal HTTP server for internal service-to-service calls:
158
+
159
+ ```typescript
160
+ import { createAgentServer } from "@msm-core/mini";
161
+
162
+ const server = createAgentServer(agent, { port: 4001 });
163
+ await server.start();
164
+ // GET /health → { ok: true, uptime: N }
165
+ // POST /v1/event → { sessionId, message } → LoopOutcome JSON
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Guards
171
+
172
+ Guards are hard limits that terminate the loop and return a forced response. Configure in the agent definition `## Limits` section or override in code:
173
+
174
+ | Guard | Default | Description |
175
+ | ------------------------ | ---------- | --------------------------------------- |
176
+ | `maxIterations` | 60 | Max brain-call iterations per task |
177
+ | `costCapPerTask` | $2.50 | Max spend per task (USD) |
178
+ | `timeoutMs` | 900 000 ms | Wall-clock timeout |
179
+ | `maxToolCallsPerTask` | 200 | Max tool invocations |
180
+ | `maxConsecutiveFailures` | 3 | Abort after N consecutive tool failures |
181
+
182
+ ---
183
+
184
+ ## Redis Schema
185
+
186
+ ```
187
+ session:{prefix}:{id}:history # Message history (TTL: 24h)
188
+ session:{prefix}:{id}:metadata # Iteration count, cost accumulator
189
+ session:{prefix}:{id}:tools:dedup # Tool-call dedup cache (TTL: 5min)
190
+ agent:{prefix}:control:{id} # Kill/pause signal
191
+ ```
192
+
193
+ No long-term storage — Redis is ephemeral session cache only. Your app owns persistence.
194
+
195
+ ---
196
+
197
+ ## Architecture
198
+
199
+ ```
200
+ Your App
201
+
202
+ ├── assembles context (via @msm-core/context)
203
+ ├── calls agent.handle({ sessionId, message, context })
204
+
205
+ └── @msm-core/mini
206
+ ├── gates (ack, business hours)
207
+ ├── session lock (Redis distributed)
208
+ ├── context builder (persona + memories + tools + history)
209
+ ├── brain call (Gemini / OpenAI / Anthropic / Ollama)
210
+ ├── guards (iterations, cost, timeout, failures)
211
+ └── tool executor (validate → dedup check → execute → record)
212
+
213
+ └── Your Tools (passed explicitly — no registries)
214
+ ```
215
+
216
+ ---
217
+
218
+ ## API Reference
219
+
220
+ ### `createAgent(config)`
221
+
222
+ ```typescript
223
+ interface AgentConfig {
224
+ definition: string | AgentDefinition; // .md file path or parsed object
225
+ brain: Brain; // from createGeminiBrain() etc.
226
+ redis: {
227
+ url: string;
228
+ prefix?: string; // key namespace (default: "msm")
229
+ ttl?: { history?: number; document?: number };
230
+ };
231
+ tools: Tool[];
232
+ guards?: Partial<GuardConfig>;
233
+ gates?: GateConfig;
234
+ hooks?: AgentHooks; // lifecycle callbacks
235
+ }
236
+ ```
237
+
238
+ ### `agent.handle(event)`
239
+
240
+ ```typescript
241
+ interface AgentEvent {
242
+ sessionId: string;
243
+ message: string;
244
+ context?: {
245
+ systemPrompt?: string;
246
+ memories?: MemoryEntry[];
247
+ userProfile?: Record<string, unknown>;
248
+ customFields?: Record<string, unknown>;
249
+ };
250
+ }
251
+ // Returns: Promise<LoopOutcome>
252
+ ```
253
+
254
+ ### `LoopOutcome`
255
+
256
+ ```typescript
257
+ interface LoopOutcome {
258
+ type:
259
+ | "response"
260
+ | "tool_calls"
261
+ | "error"
262
+ | "clarify"
263
+ | "escalate"
264
+ | "suppressed";
265
+ sessionId: string;
266
+ taskId: string;
267
+ text?: string;
268
+ metrics: {
269
+ iterations: number;
270
+ totalCostUsd: number;
271
+ durationMs: number;
272
+ totalToolCalls: number;
273
+ };
274
+ terminatedBy?: string; // guard type that ended the loop, if any
275
+ }
276
+ ```
277
+
278
+ ---
279
+
280
+ ## Migrating from `msm-agent`
281
+
282
+ | msm-agent | @msm-core/mini |
283
+ | ------------------------------- | ----------------------------------------------- |
284
+ | `loadAgent(path)` | `parseDefinition(path)` |
285
+ | `SQLiteMemoryAdapter` | ❌ removed — app owns DB |
286
+ | `MongoMemoryAdapter` | ❌ removed — app owns DB |
287
+ | `QdrantKnowledgeAdapter` | Use `@msm-core/context` + `QdrantAdapter` |
288
+ | `SkillRegistry.register(skill)` | Pass tools directly to `createAgent({ tools })` |
289
+ | `createAgentHub(...)` | ❌ removed |
290
+ | `createMcpServer(...)` | ❌ removed |
291
+ | `ManualEventAdapter` | Call `agent.handle()` directly |
292
+
293
+ ---
294
+
295
+ ## License
296
+
297
+ MIT
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Adapters barrel — re-exports all adapter classes for convenience.
3
+ */
4
+ export { RedisMemory, connectRedis } from "./redis-memory.js";
5
+ export type { SessionMetadata } from "./redis-memory.js";
6
+ export { RedisControlBus } from "./redis-control.js";
7
+ export { RedisDistributedLock } from "./redis-lock.js";
8
+ export type { LockHandle } from "./redis-lock.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9D,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Adapters barrel — re-exports all adapter classes for convenience.
3
+ */
4
+ export { RedisMemory, connectRedis } from "./redis-memory.js";
5
+ export { RedisControlBus } from "./redis-control.js";
6
+ export { RedisDistributedLock } from "./redis-lock.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Redis Control Bus — Kill / pause / disable-tool commands.
3
+ *
4
+ * Keys:
5
+ * {prefix}:agent:control:{sessionId} → "kill" | "pause" | "disabled:{toolName}"
6
+ *
7
+ * Checked on every loop iteration before the brain call.
8
+ */
9
+ import type { RedisLike } from "./redis-types.js";
10
+ export declare class RedisControlBus {
11
+ private readonly redis;
12
+ private readonly prefix;
13
+ constructor(redis: RedisLike, prefix?: string);
14
+ private controlKey;
15
+ /** Read the current control command for a session. Returns null if none. */
16
+ getCommand(sessionId: string): Promise<string | null>;
17
+ kill(sessionId: string, reason?: string): Promise<void>;
18
+ pause(sessionId: string): Promise<void>;
19
+ resume(sessionId: string): Promise<void>;
20
+ disableTool(sessionId: string, toolName: string): Promise<void>;
21
+ clear(sessionId: string): Promise<void>;
22
+ /** Returns the disabled tool name if the command is disable, else null */
23
+ static disabledTool(command: string | null): string | null;
24
+ }
25
+ //# sourceMappingURL=redis-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-control.d.ts","sourceRoot":"","sources":["../../src/adapters/redis-control.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAIlD,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,KAAK,EAAE,SAAS,EAChB,MAAM,GAAE,MAAc;IAGzC,OAAO,CAAC,UAAU;IAIlB,4EAA4E;IACtE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIrD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,SAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxC,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/D,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,0EAA0E;IAC1E,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI;CAI3D"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Redis Control Bus — Kill / pause / disable-tool commands.
3
+ *
4
+ * Keys:
5
+ * {prefix}:agent:control:{sessionId} → "kill" | "pause" | "disabled:{toolName}"
6
+ *
7
+ * Checked on every loop iteration before the brain call.
8
+ */
9
+ const KILLED_TTL = 7 * 24 * 60 * 60; // 7 days
10
+ export class RedisControlBus {
11
+ redis;
12
+ prefix;
13
+ constructor(redis, prefix = "msm") {
14
+ this.redis = redis;
15
+ this.prefix = prefix;
16
+ }
17
+ controlKey(sessionId) {
18
+ return `${this.prefix}:agent:control:${sessionId}`;
19
+ }
20
+ /** Read the current control command for a session. Returns null if none. */
21
+ async getCommand(sessionId) {
22
+ return this.redis.get(this.controlKey(sessionId));
23
+ }
24
+ async kill(sessionId, reason = "killed") {
25
+ await this.redis.setex(this.controlKey(sessionId), KILLED_TTL, `kill:${reason}`);
26
+ }
27
+ async pause(sessionId) {
28
+ await this.redis.set(this.controlKey(sessionId), "pause");
29
+ }
30
+ async resume(sessionId) {
31
+ const cmd = await this.getCommand(sessionId);
32
+ if (cmd === "pause")
33
+ await this.redis.del(this.controlKey(sessionId));
34
+ }
35
+ async disableTool(sessionId, toolName) {
36
+ await this.redis.set(this.controlKey(sessionId), `disabled:${toolName}`);
37
+ }
38
+ async clear(sessionId) {
39
+ await this.redis.del(this.controlKey(sessionId));
40
+ }
41
+ /** Returns the disabled tool name if the command is disable, else null */
42
+ static disabledTool(command) {
43
+ if (!command?.startsWith("disabled:"))
44
+ return null;
45
+ return command.slice("disabled:".length);
46
+ }
47
+ }
48
+ //# sourceMappingURL=redis-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-control.js","sourceRoot":"","sources":["../../src/adapters/redis-control.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,SAAS;AAE9C,MAAM,OAAO,eAAe;IAEP;IACA;IAFnB,YACmB,KAAgB,EAChB,SAAiB,KAAK;QADtB,UAAK,GAAL,KAAK,CAAW;QAChB,WAAM,GAAN,MAAM,CAAgB;IACtC,CAAC;IAEI,UAAU,CAAC,SAAiB;QAClC,OAAO,GAAG,IAAI,CAAC,MAAM,kBAAkB,SAAS,EAAE,CAAC;IACrD,CAAC;IAED,4EAA4E;IAC5E,KAAK,CAAC,UAAU,CAAC,SAAiB;QAChC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,SAAiB,EAAE,MAAM,GAAG,QAAQ;QAC7C,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,QAAQ,MAAM,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,SAAiB;QAC3B,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,SAAiB;QAC5B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,GAAG,KAAK,OAAO;YAAE,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,SAAiB,EAAE,QAAgB;QACnD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,YAAY,QAAQ,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,SAAiB;QAC3B,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,0EAA0E;IAC1E,MAAM,CAAC,YAAY,CAAC,OAAsB;QACxC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,WAAW,CAAC;YAAE,OAAO,IAAI,CAAC;QACnD,OAAO,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;CACF"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Redis Distributed Lock — Atomic session mutex.
3
+ *
4
+ * Uses SET NX PX (atomic set-if-not-exists with TTL).
5
+ * Returns a LockHandle with release() and extend() for long operations.
6
+ *
7
+ * Key: {prefix}:session:{id}:lock
8
+ * TTL: timeoutMs + 30s safety margin
9
+ */
10
+ import type { RedisLike } from "./redis-types.js";
11
+ export interface LockHandle {
12
+ release(): Promise<void>;
13
+ extend(ttlMs: number): Promise<boolean>;
14
+ }
15
+ export declare class RedisDistributedLock {
16
+ private readonly redis;
17
+ private readonly prefix;
18
+ constructor(redis: RedisLike, prefix?: string);
19
+ private lockKey;
20
+ /**
21
+ * Attempt to acquire the lock.
22
+ * Returns a LockHandle on success, or null if already held.
23
+ */
24
+ acquire(sessionId: string, ttlMs: number): Promise<LockHandle | null>;
25
+ /**
26
+ * Acquire with retry (exponential backoff).
27
+ * Throws if the lock cannot be acquired within `waitMs`.
28
+ */
29
+ acquireWithRetry(sessionId: string, ttlMs: number, waitMs?: number, retryInterval?: number): Promise<LockHandle>;
30
+ }
31
+ //# sourceMappingURL=redis-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-lock.d.ts","sourceRoot":"","sources":["../../src/adapters/redis-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAkBlD,MAAM,WAAW,UAAU;IACzB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACzC;AAED,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,KAAK,EAAE,SAAS,EAChB,MAAM,GAAE,MAAc;IAGzC,OAAO,CAAC,OAAO;IAIf;;;OAGG;IACG,OAAO,CACX,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAiB7B;;;OAGG;IACG,gBAAgB,CACpB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,SAAO,EACb,aAAa,SAAM,GAClB,OAAO,CAAC,UAAU,CAAC;CASvB"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Redis Distributed Lock — Atomic session mutex.
3
+ *
4
+ * Uses SET NX PX (atomic set-if-not-exists with TTL).
5
+ * Returns a LockHandle with release() and extend() for long operations.
6
+ *
7
+ * Key: {prefix}:session:{id}:lock
8
+ * TTL: timeoutMs + 30s safety margin
9
+ */
10
+ import { randomBytes } from "crypto";
11
+ const RELEASE_SCRIPT = `
12
+ if redis.call("get", KEYS[1]) == ARGV[1] then
13
+ return redis.call("del", KEYS[1])
14
+ else
15
+ return 0
16
+ end
17
+ `;
18
+ const EXTEND_SCRIPT = `
19
+ if redis.call("get", KEYS[1]) == ARGV[1] then
20
+ return redis.call("pexpire", KEYS[1], ARGV[2])
21
+ else
22
+ return 0
23
+ end
24
+ `;
25
+ export class RedisDistributedLock {
26
+ redis;
27
+ prefix;
28
+ constructor(redis, prefix = "msm") {
29
+ this.redis = redis;
30
+ this.prefix = prefix;
31
+ }
32
+ lockKey(sessionId) {
33
+ return `${this.prefix}:session:${sessionId}:lock`;
34
+ }
35
+ /**
36
+ * Attempt to acquire the lock.
37
+ * Returns a LockHandle on success, or null if already held.
38
+ */
39
+ async acquire(sessionId, ttlMs) {
40
+ const key = this.lockKey(sessionId);
41
+ const token = randomBytes(16).toString("hex");
42
+ const result = await this.redis.set(key, token, "PX", ttlMs, "NX");
43
+ if (!result)
44
+ return null;
45
+ return {
46
+ release: async () => {
47
+ await this.redis.eval(RELEASE_SCRIPT, 1, key, token);
48
+ },
49
+ extend: async (newTtlMs) => {
50
+ const r = await this.redis.eval(EXTEND_SCRIPT, 1, key, token, String(newTtlMs));
51
+ return r === 1;
52
+ },
53
+ };
54
+ }
55
+ /**
56
+ * Acquire with retry (exponential backoff).
57
+ * Throws if the lock cannot be acquired within `waitMs`.
58
+ */
59
+ async acquireWithRetry(sessionId, ttlMs, waitMs = 5000, retryInterval = 100) {
60
+ const deadline = Date.now() + waitMs;
61
+ while (Date.now() < deadline) {
62
+ const handle = await this.acquire(sessionId, ttlMs);
63
+ if (handle)
64
+ return handle;
65
+ await sleep(retryInterval + Math.random() * 50);
66
+ }
67
+ throw new Error(`msm-mini: could not acquire lock for session ${sessionId} within ${waitMs}ms`);
68
+ }
69
+ }
70
+ function sleep(ms) {
71
+ return new Promise((resolve) => setTimeout(resolve, ms));
72
+ }
73
+ //# sourceMappingURL=redis-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-lock.js","sourceRoot":"","sources":["../../src/adapters/redis-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAGrC,MAAM,cAAc,GAAG;;;;;;CAMtB,CAAC;AAEF,MAAM,aAAa,GAAG;;;;;;CAMrB,CAAC;AAOF,MAAM,OAAO,oBAAoB;IAEZ;IACA;IAFnB,YACmB,KAAgB,EAChB,SAAiB,KAAK;QADtB,UAAK,GAAL,KAAK,CAAW;QAChB,WAAM,GAAN,MAAM,CAAgB;IACtC,CAAC;IAEI,OAAO,CAAC,SAAiB;QAC/B,OAAO,GAAG,IAAI,CAAC,MAAM,YAAY,SAAS,OAAO,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CACX,SAAiB,EACjB,KAAa;QAEb,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,OAAO;YACL,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YACvD,CAAC;YACD,MAAM,EAAE,KAAK,EAAE,QAAgB,EAAE,EAAE;gBACjC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAChF,OAAO,CAAC,KAAK,CAAC,CAAC;YACjB,CAAC;SACF,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,gBAAgB,CACpB,SAAiB,EACjB,KAAa,EACb,MAAM,GAAG,IAAI,EACb,aAAa,GAAG,GAAG;QAEnB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC;QACrC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YACpD,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAC;YAC1B,MAAM,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,gDAAgD,SAAS,WAAW,MAAM,IAAI,CAAC,CAAC;IAClG,CAAC;CACF;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Redis Memory Adapter — Session history, metadata, document state.
3
+ *
4
+ * The only memory store in msm-mini. All long-term persistence stays
5
+ * in the application layer.
6
+ *
7
+ * Keys (all prefixed with config.prefix):
8
+ * {prefix}:session:{id}:history JSON array, TTL 24h
9
+ * {prefix}:session:{id}:metadata JSON object, TTL 24h
10
+ * {prefix}:session:{id}:document JSON object, TTL 4h
11
+ */
12
+ import type { Message, DocumentState } from "../core/types.js";
13
+ import type { RedisLike } from "./redis-types.js";
14
+ export interface SessionMetadata {
15
+ iterationCount: number;
16
+ startedAt: number;
17
+ totalCostUsd: number;
18
+ status: "running" | "completed" | "failed" | "killed";
19
+ }
20
+ export declare class RedisMemory {
21
+ private readonly redis;
22
+ private readonly prefix;
23
+ private readonly historyTtl;
24
+ private readonly documentTtl;
25
+ constructor(redis: RedisLike, opts?: {
26
+ prefix?: string;
27
+ historyTtl?: number;
28
+ documentTtl?: number;
29
+ });
30
+ private historyKey;
31
+ private metaKey;
32
+ private documentKey;
33
+ appendHistory(sessionId: string, entry: Message): Promise<void>;
34
+ getHistory(sessionId: string, limit?: number): Promise<Message[]>;
35
+ getMetadata(sessionId: string): Promise<SessionMetadata | null>;
36
+ setMetadata(sessionId: string, meta: SessionMetadata): Promise<void>;
37
+ getDocumentState(sessionId: string): Promise<DocumentState | null>;
38
+ setDocumentState(sessionId: string, state: DocumentState): Promise<void>;
39
+ }
40
+ /** Connect to Redis using ioredis (peer dependency) */
41
+ export declare function connectRedis(url: string): Promise<RedisLike>;
42
+ //# sourceMappingURL=redis-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-memory.d.ts","sourceRoot":"","sources":["../../src/adapters/redis-memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACvD;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGnC,KAAK,EAAE,SAAS,EAChB,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO;IAQ3E,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,WAAW;IAMb,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/D,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAc7D,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAU/D,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAUpE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAUlE,gBAAgB,CACpB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC,IAAI,CAAC;CAOjB;AAED,uDAAuD;AACvD,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAMlE"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Redis Memory Adapter — Session history, metadata, document state.
3
+ *
4
+ * The only memory store in msm-mini. All long-term persistence stays
5
+ * in the application layer.
6
+ *
7
+ * Keys (all prefixed with config.prefix):
8
+ * {prefix}:session:{id}:history JSON array, TTL 24h
9
+ * {prefix}:session:{id}:metadata JSON object, TTL 24h
10
+ * {prefix}:session:{id}:document JSON object, TTL 4h
11
+ */
12
+ export class RedisMemory {
13
+ redis;
14
+ prefix;
15
+ historyTtl;
16
+ documentTtl;
17
+ constructor(redis, opts = {}) {
18
+ this.redis = redis;
19
+ this.prefix = opts.prefix ?? "msm";
20
+ this.historyTtl = opts.historyTtl ?? 86_400;
21
+ this.documentTtl = opts.documentTtl ?? 14_400;
22
+ }
23
+ historyKey(sessionId) {
24
+ return `${this.prefix}:session:${sessionId}:history`;
25
+ }
26
+ metaKey(sessionId) {
27
+ return `${this.prefix}:session:${sessionId}:metadata`;
28
+ }
29
+ documentKey(sessionId) {
30
+ return `${this.prefix}:session:${sessionId}:document`;
31
+ }
32
+ // ─── History ──────────────────────────────────────────────
33
+ async appendHistory(sessionId, entry) {
34
+ const key = this.historyKey(sessionId);
35
+ await this.redis.rpush(key, JSON.stringify(entry));
36
+ await this.redis.expire(key, this.historyTtl);
37
+ }
38
+ async getHistory(sessionId, limit = 50) {
39
+ const key = this.historyKey(sessionId);
40
+ const raw = await this.redis.lrange(key, -limit, -1);
41
+ return raw.flatMap((r) => {
42
+ try {
43
+ return [JSON.parse(r)];
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ });
49
+ }
50
+ // ─── Metadata ─────────────────────────────────────────────
51
+ async getMetadata(sessionId) {
52
+ const raw = await this.redis.get(this.metaKey(sessionId));
53
+ if (!raw)
54
+ return null;
55
+ try {
56
+ return JSON.parse(raw);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ async setMetadata(sessionId, meta) {
63
+ await this.redis.setex(this.metaKey(sessionId), this.historyTtl, JSON.stringify(meta));
64
+ }
65
+ // ─── Document State (section tracker) ────────────────────
66
+ async getDocumentState(sessionId) {
67
+ const raw = await this.redis.get(this.documentKey(sessionId));
68
+ if (!raw)
69
+ return null;
70
+ try {
71
+ return JSON.parse(raw);
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ async setDocumentState(sessionId, state) {
78
+ await this.redis.setex(this.documentKey(sessionId), this.documentTtl, JSON.stringify(state));
79
+ }
80
+ }
81
+ /** Connect to Redis using ioredis (peer dependency) */
82
+ export async function connectRedis(url) {
83
+ const mod = await import("ioredis");
84
+ const Redis = (mod.default ?? mod);
85
+ return new Redis(url);
86
+ }
87
+ //# sourceMappingURL=redis-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-memory.js","sourceRoot":"","sources":["../../src/adapters/redis-memory.ts"],"names":[],"mappings":"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"}