@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.
- package/README.md +297 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/redis-control.d.ts +25 -0
- package/dist/adapters/redis-control.d.ts.map +1 -0
- package/dist/adapters/redis-control.js +48 -0
- package/dist/adapters/redis-control.js.map +1 -0
- package/dist/adapters/redis-lock.d.ts +31 -0
- package/dist/adapters/redis-lock.d.ts.map +1 -0
- package/dist/adapters/redis-lock.js +73 -0
- package/dist/adapters/redis-lock.js.map +1 -0
- package/dist/adapters/redis-memory.d.ts +42 -0
- package/dist/adapters/redis-memory.d.ts.map +1 -0
- package/dist/adapters/redis-memory.js +87 -0
- package/dist/adapters/redis-memory.js.map +1 -0
- package/dist/adapters/redis-types.d.ts +17 -0
- package/dist/adapters/redis-types.d.ts.map +1 -0
- package/dist/adapters/redis-types.js +2 -0
- package/dist/adapters/redis-types.js.map +1 -0
- package/dist/brain/anthropic.d.ts +10 -0
- package/dist/brain/anthropic.d.ts.map +1 -0
- package/dist/brain/anthropic.js +73 -0
- package/dist/brain/anthropic.js.map +1 -0
- package/dist/brain/factory.d.ts +7 -0
- package/dist/brain/factory.d.ts.map +1 -0
- package/dist/brain/factory.js +24 -0
- package/dist/brain/factory.js.map +1 -0
- package/dist/brain/gemini.d.ts +10 -0
- package/dist/brain/gemini.d.ts.map +1 -0
- package/dist/brain/gemini.js +113 -0
- package/dist/brain/gemini.js.map +1 -0
- package/dist/brain/ollama.d.ts +10 -0
- package/dist/brain/ollama.d.ts.map +1 -0
- package/dist/brain/ollama.js +38 -0
- package/dist/brain/ollama.js.map +1 -0
- package/dist/brain/openai.d.ts +11 -0
- package/dist/brain/openai.d.ts.map +1 -0
- package/dist/brain/openai.js +96 -0
- package/dist/brain/openai.js.map +1 -0
- package/dist/brain/types.d.ts +6 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +2 -0
- package/dist/brain/types.js.map +1 -0
- package/dist/core/context-builder.d.ts +25 -0
- package/dist/core/context-builder.d.ts.map +1 -0
- package/dist/core/context-builder.js +107 -0
- package/dist/core/context-builder.js.map +1 -0
- package/dist/core/gates.d.ts +16 -0
- package/dist/core/gates.d.ts.map +1 -0
- package/dist/core/gates.js +70 -0
- package/dist/core/gates.js.map +1 -0
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +119 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/hooks.d.ts +13 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +61 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/loop.d.ts +16 -0
- package/dist/core/loop.d.ts.map +1 -0
- package/dist/core/loop.js +255 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/core/types.d.ts +310 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -0
- package/dist/definition/parser.d.ts +12 -0
- package/dist/definition/parser.d.ts.map +1 -0
- package/dist/definition/parser.js +117 -0
- package/dist/definition/parser.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/quality/scorer.d.ts +18 -0
- package/dist/quality/scorer.d.ts.map +1 -0
- package/dist/quality/scorer.js +22 -0
- package/dist/quality/scorer.js.map +1 -0
- package/dist/server.d.ts +27 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +118 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/dedup.d.ts +20 -0
- package/dist/tools/dedup.d.ts.map +1 -0
- package/dist/tools/dedup.js +47 -0
- package/dist/tools/dedup.js.map +1 -0
- package/dist/tools/executor.d.ts +32 -0
- package/dist/tools/executor.d.ts.map +1 -0
- package/dist/tools/executor.js +75 -0
- package/dist/tools/executor.js.map +1 -0
- 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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"}
|