@game_ryo/lsji 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +90 -21
- package/README.md +15 -15
- package/package.json +3 -2
- package/src/cli.js +11 -4
- package/src/execution/hitl/approval-gate.js +3 -1
- package/src/execution/idempotency.js +4 -2
- package/src/index.js +1 -1
- package/src/llm/llm-agent.js +30 -7
- package/src/llm/memory/conversation.js +11 -10
- package/src/llm/prompt-manager.js +58 -139
- package/src/server/index.js +6 -11
- package/src/storage/better-sqlite.js +31 -0
- package/src/storage/index.js +50 -0
- package/src/storage/memory.js +188 -0
- package/src/storage/sqlite.js +31 -0
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**LSJI** (Learning System for JavaScript Intelligence) is a general-purpose Reinforcement Learning agent framework for Node.js. It provides a clean abstraction for building RL agents with pluggable storage backends, environments, and learning algorithms.
|
|
6
6
|
|
|
7
|
-
Originally migrated from a Cloudflare Workers implementation (Rock-Paper-Scissors AI), now redesigned as a standalone npm package with
|
|
7
|
+
Originally migrated from a Cloudflare Workers implementation (Rock-Paper-Scissors AI), now redesigned as a standalone npm package with MIT license, targeting MIT licensed.
|
|
8
8
|
|
|
9
9
|
## Architecture
|
|
10
10
|
|
|
@@ -19,6 +19,23 @@ src/
|
|
|
19
19
|
│ ├── sqlite.js # node:sqlite implementation (Node 22+)
|
|
20
20
|
│ ├── better-sqlite.js # better-sqlite3 implementation
|
|
21
21
|
│ └── memory.js # In-memory implementation
|
|
22
|
+
├── execution/ # Production execution systems
|
|
23
|
+
│ ├── budget/ # TokenCounter, CostTracker, CircuitBreaker
|
|
24
|
+
│ ├── hitl/ # ApprovalGate, ApprovalStore, Notifier
|
|
25
|
+
│ ├── idempotency.js # Duplicate prevention
|
|
26
|
+
│ └── engine.js # Durable execution with checkpoints
|
|
27
|
+
├── llm/ # LLM Agent Framework
|
|
28
|
+
│ ├── providers/ # OpenAI, Anthropic, Gemini, Local (Ollama)
|
|
29
|
+
│ ├── llm-agent.js # ReAct agent with tools & memory
|
|
30
|
+
│ ├── tools/ # Built-in tools (web_search, file_read, file_write, code_exec, api_call, send_email, db_query)
|
|
31
|
+
│ ├── memory/ # Conversation, Semantic, Episodic
|
|
32
|
+
│ ├── plugins/ # Dynamic plugin system
|
|
33
|
+
│ └── prompt-manager.js
|
|
34
|
+
├── server/ # Runtime server + Web UI
|
|
35
|
+
│ ├── index.js # Express + Socket.io
|
|
36
|
+
│ └── ui/ # React + Vite control panel
|
|
37
|
+
├── envs/
|
|
38
|
+
│ └── rps.js # Rock-Paper-Scissors environment
|
|
22
39
|
├── cli.js # Command-line interface
|
|
23
40
|
└── index.js # Public API exports
|
|
24
41
|
```
|
|
@@ -59,9 +76,9 @@ Pluggable backends:
|
|
|
59
76
|
|
|
60
77
|
## Usage
|
|
61
78
|
|
|
62
|
-
### As Library
|
|
79
|
+
### As Library (RL)
|
|
63
80
|
```javascript
|
|
64
|
-
import { Agent, QLearning, createStorage, Env } from 'lsji';
|
|
81
|
+
import { Agent, QLearning, createStorage, Env } from '@game_ryo/lsji';
|
|
65
82
|
|
|
66
83
|
// Create custom environment
|
|
67
84
|
class MyEnv extends Env {
|
|
@@ -77,23 +94,60 @@ await agent.train({ episodes: 1000 });
|
|
|
77
94
|
const result = await agent.play(userAction);
|
|
78
95
|
```
|
|
79
96
|
|
|
97
|
+
### As Library (LLM Agent)
|
|
98
|
+
```javascript
|
|
99
|
+
import { createLLMAgent, startServer } from '@game_ryo/lsji';
|
|
100
|
+
|
|
101
|
+
// Create an agent with full production features
|
|
102
|
+
const agent = await createLLMAgent({
|
|
103
|
+
llm: { provider: 'gemini', model: 'gemini-1.5-flash', apiKey: process.env.GEMINI_API_KEY },
|
|
104
|
+
budget: { maxCostPerRun: 5, maxCostPerDay: 50 },
|
|
105
|
+
hitl: { enabled: true, defaultTimeout: 300000 }, // 5 min approval timeout
|
|
106
|
+
memory: { conversation: true, episodic: true, semantic: true },
|
|
107
|
+
storage: { type: 'sqlite', options: { path: './lsji.db' } },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await agent.run("Research TypeScript best practices and create a summary");
|
|
111
|
+
console.log(result.answer);
|
|
112
|
+
|
|
113
|
+
// Or start the full runtime server
|
|
114
|
+
const server = await startServer({ port: 3456 });
|
|
115
|
+
// Visit http://localhost:3456 for the web control panel
|
|
116
|
+
```
|
|
117
|
+
|
|
80
118
|
### CLI
|
|
81
119
|
```bash
|
|
82
120
|
# Install globally or use npx
|
|
83
121
|
npm link # for local development
|
|
84
122
|
|
|
85
|
-
#
|
|
86
|
-
lsji train --episodes 500 --pattern 0
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
lsji
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
lsji
|
|
96
|
-
|
|
123
|
+
# RL Commands
|
|
124
|
+
npx @game_ryo/lsji train --episodes 500 --pattern 0
|
|
125
|
+
npx @game_ryo/lsji play --hand 0 # 0=Rock, 1=Scissors, 2=Paper
|
|
126
|
+
npx @game_ryo/lsji status --json
|
|
127
|
+
npx @game_ryo/lsji start
|
|
128
|
+
npx @game_ryo/lsji stop
|
|
129
|
+
|
|
130
|
+
# LLM Agent Commands
|
|
131
|
+
npx @game_ryo/lsji agent run --task "Search for latest AI news and summarize" --provider gemini --model gemini-1.5-flash
|
|
132
|
+
npx @game_ryo/lsji agent run-durable --task "Analyze codebase and create report" --workflow-id my-analysis
|
|
133
|
+
npx @game_ryo/lsji agent status
|
|
134
|
+
|
|
135
|
+
# Budget & Approvals
|
|
136
|
+
npx @game_ryo/lsji budget status --budgetId my-project
|
|
137
|
+
npx @game_ryo/lsji hitl list # Pending approvals
|
|
138
|
+
npx @game_ryo/lsji hitl approve --id <id> --reason "Approved"
|
|
139
|
+
|
|
140
|
+
# Durability
|
|
141
|
+
npx @game_ryo/lsji checkpoint list
|
|
142
|
+
npx @game_ryo/lsji checkpoint show --workflowId my-analysis
|
|
143
|
+
npx @game_ryo/lsji checkpoint recover --workflowId my-analysis
|
|
144
|
+
|
|
145
|
+
# Plugins
|
|
146
|
+
npx @game_ryo/lsji plugin list
|
|
147
|
+
npx @game_ryo/lsji plugin create --name my-tools
|
|
148
|
+
|
|
149
|
+
# Server
|
|
150
|
+
npx @game_ryo/lsji serve --port 3456
|
|
97
151
|
```
|
|
98
152
|
|
|
99
153
|
## Development
|
|
@@ -102,20 +156,20 @@ lsji stop
|
|
|
102
156
|
```bash
|
|
103
157
|
npm test # Run tests (vitest)
|
|
104
158
|
npm run build # No build step (ESM)
|
|
159
|
+
npm run build:ui # Build web UI
|
|
160
|
+
npm run serve # Start runtime server
|
|
105
161
|
```
|
|
106
162
|
|
|
107
163
|
### Testing
|
|
108
|
-
- Tests use `vitest` with `
|
|
109
|
-
- Run `npm test` to verify all functionality
|
|
110
|
-
|
|
111
|
-
## Key Design Decisions
|
|
164
|
+
- Tests use `vitest` with `Memo## Key Design Decisions
|
|
112
165
|
|
|
113
166
|
1. **ESM only** - Uses `"type": "module"` in package.json
|
|
114
167
|
2. **Node 22+** - Requires Node 22 for built-in `node:sqlite`
|
|
115
|
-
3. **
|
|
168
|
+
3. **MIT** - License compatible with open source
|
|
116
169
|
4. **Single package** - All core functionality in one npm package
|
|
117
170
|
5. **Storage abstraction** - Easy to swap backends
|
|
118
171
|
6. **Worker.js compatibility** - Training patterns and reward logic match original
|
|
172
|
+
7. **Production-grade** - HITL, durability, budget controls, idempotency built-in
|
|
119
173
|
|
|
120
174
|
## Common Tasks
|
|
121
175
|
|
|
@@ -129,6 +183,11 @@ npm run build # No build step (ESM)
|
|
|
129
183
|
2. Implement all abstract methods
|
|
130
184
|
3. Add to `createStorage` factory
|
|
131
185
|
|
|
186
|
+
### Adding a New LLM Tool
|
|
187
|
+
1. Create tool definition in `src/llm/tools/registry.js` or plugin
|
|
188
|
+
2. Implement `execute` function
|
|
189
|
+
3. Add to tool registry
|
|
190
|
+
|
|
132
191
|
### Modifying Learning Algorithm
|
|
133
192
|
- Extend `QLearning` class or create new algorithm in `src/core/`
|
|
134
193
|
|
|
@@ -140,4 +199,14 @@ npm run build # No build step (ESM)
|
|
|
140
199
|
|
|
141
200
|
## License
|
|
142
201
|
|
|
143
|
-
|
|
202
|
+
MIT - see LICENSE file
|
|
203
|
+
|
|
204
|
+
## Git Workflow
|
|
205
|
+
|
|
206
|
+
- Commit messages in English
|
|
207
|
+
- Format: `<type>: <subject>` (e.g., `feat: add new environment base class`)
|
|
208
|
+
- Types: feat, fix, docs, refactor, test, chore
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT - see LICENSE file
|
package/README.md
CHANGED
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
# Install
|
|
28
|
-
npm install lsji
|
|
28
|
+
npm install @game_ryo/lsji
|
|
29
29
|
|
|
30
30
|
# Set your API key (Gemini, OpenAI, or Anthropic)
|
|
31
31
|
export GEMINI_API_KEY="your-gemini-key"
|
|
32
32
|
# or: export OPENAI_API_KEY="your-openai-key"
|
|
33
33
|
|
|
34
34
|
# Start the runtime server with web UI
|
|
35
|
-
npx lsji serve
|
|
35
|
+
npx @game_ryo/npx @game_ryo/lsji serve
|
|
36
36
|
|
|
37
37
|
# Open http://localhost:3456 — create runs, watch thought logs, approve actions
|
|
38
38
|
```
|
|
@@ -41,25 +41,25 @@ npx lsji serve
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Runtime server with control panel
|
|
44
|
-
lsji serve --port 3456 # Start server + UI
|
|
45
|
-
lsji serve --no-ui # Headless mode
|
|
44
|
+
npx @game_ryo/lsji serve --port 3456 # Start server + UI
|
|
45
|
+
npx @game_ryo/lsji serve --no-ui # Headless mode
|
|
46
46
|
|
|
47
47
|
# Agent operations
|
|
48
|
-
lsji agent run --task "Write a report on AI trends" --provider gemini --model gemini-1.5-flash
|
|
49
|
-
lsji agent run-durable --task "Analyze codebase" --workflow-id my-analysis
|
|
48
|
+
npx @game_ryo/lsji agent run --task "Write a report on AI trends" --provider gemini --model gemini-1.5-flash
|
|
49
|
+
npx @game_ryo/lsji agent run-durable --task "Analyze codebase" --workflow-id my-analysis
|
|
50
50
|
|
|
51
51
|
# Budget & approvals
|
|
52
|
-
lsji budget status --budgetId my-project
|
|
53
|
-
lsji hitl list # Pending approvals
|
|
54
|
-
lsji hitl approve --id <id> --reason "Approved"
|
|
52
|
+
npx @game_ryo/lsji budget status --budgetId my-project
|
|
53
|
+
npx @game_ryo/lsji hitl list # Pending approvals
|
|
54
|
+
npx @game_ryo/lsji hitl approve --id <id> --reason "Approved"
|
|
55
55
|
|
|
56
56
|
# Plugins
|
|
57
|
-
lsji plugin list
|
|
58
|
-
lsji plugin create --name my-tools # Generate template
|
|
57
|
+
npx @game_ryo/lsji plugin list
|
|
58
|
+
npx @game_ryo/lsji plugin create --name my-tools # Generate template
|
|
59
59
|
|
|
60
60
|
# RL (legacy)
|
|
61
|
-
lsji train --episodes 500
|
|
62
|
-
lsji play --hand 0
|
|
61
|
+
npx @game_ryo/lsji train --episodes 500
|
|
62
|
+
npx @game_ryo/lsji play --hand 0
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## Programmatic Usage
|
|
@@ -85,7 +85,7 @@ const server = await startServer({ port: 3456 });
|
|
|
85
85
|
|
|
86
86
|
## Web Control Panel
|
|
87
87
|
|
|
88
|
-
When you run `lsji serve`, you get a real-time dashboard at **http://localhost:3456**:
|
|
88
|
+
When you run `npx @game_ryo/lsji serve`, you get a real-time dashboard at **http://localhost:3456**:
|
|
89
89
|
|
|
90
90
|
| Panel | Features |
|
|
91
91
|
|-------|----------|
|
|
@@ -117,7 +117,7 @@ export default {
|
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
lsji plugin list # Shows: my-tools (1 tools)
|
|
120
|
+
npx @game_ryo/lsji plugin list # Shows: my-tools (1 tools)
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
## Architecture
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@game_ryo/lsji",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A general-purpose reinforcement learning agent framework (Node.js) with LLM agent capabilities",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"vitest": "^2.0.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"better-sqlite3": "^13.0.3",
|
|
26
27
|
"cors": "^2.8.5",
|
|
27
28
|
"express": "^4.19.2",
|
|
28
29
|
"openai": "^7.8.0",
|
|
@@ -32,4 +33,4 @@
|
|
|
32
33
|
"publishConfig": {
|
|
33
34
|
"access": "public"
|
|
34
35
|
}
|
|
35
|
-
}
|
|
36
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -215,6 +215,7 @@ export async function main() {
|
|
|
215
215
|
|
|
216
216
|
console.log(`Running agent on task: ${task}`);
|
|
217
217
|
|
|
218
|
+
const storageConfig = { type: storageType, options: { path: dbPath } };
|
|
218
219
|
const agent = await createLLMAgent({
|
|
219
220
|
llm: {
|
|
220
221
|
provider: options.provider || 'openai',
|
|
@@ -234,6 +235,7 @@ export async function main() {
|
|
|
234
235
|
semantic: options.semantic === 'true',
|
|
235
236
|
episodic: options.episodic !== 'false',
|
|
236
237
|
},
|
|
238
|
+
storage: storageConfig,
|
|
237
239
|
});
|
|
238
240
|
|
|
239
241
|
const result = await agent.run(task, {
|
|
@@ -257,6 +259,7 @@ export async function main() {
|
|
|
257
259
|
|
|
258
260
|
console.log(`Running durable agent on task: ${task}`);
|
|
259
261
|
|
|
262
|
+
const storageConfig = { type: storageType, options: { path: dbPath } };
|
|
260
263
|
const agent = await createLLMAgent({
|
|
261
264
|
llm: {
|
|
262
265
|
provider: options.provider || 'openai',
|
|
@@ -266,6 +269,7 @@ export async function main() {
|
|
|
266
269
|
execution: {
|
|
267
270
|
checkpointInterval: parseInt(options.checkpointInterval) || 3,
|
|
268
271
|
},
|
|
272
|
+
storage: storageConfig,
|
|
269
273
|
});
|
|
270
274
|
|
|
271
275
|
const result = await agent.runDurable(task, {
|
|
@@ -280,13 +284,16 @@ export async function main() {
|
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
case 'status': {
|
|
283
|
-
const
|
|
284
|
-
|
|
287
|
+
const storageConfig = { type: storageType, options: { path: dbPath } };
|
|
288
|
+
// Create agent without initializing LLM for status check
|
|
289
|
+
const { LLMAgent } = await import('./llm/llm-agent.js');
|
|
290
|
+
const agent = new LLMAgent({
|
|
291
|
+
llm: { provider: 'local', model: 'test', baseUrl: 'http://localhost:11434/v1' },
|
|
292
|
+
storage: storageConfig,
|
|
285
293
|
});
|
|
286
|
-
|
|
294
|
+
// Don't initialize - just get status
|
|
287
295
|
const status = agent.getStatus();
|
|
288
296
|
output(status, jsonOutput);
|
|
289
|
-
await agent.shutdown();
|
|
290
297
|
break;
|
|
291
298
|
}
|
|
292
299
|
|
|
@@ -199,7 +199,9 @@ export class ApprovalGate {
|
|
|
199
199
|
* Create approval gate from config
|
|
200
200
|
*/
|
|
201
201
|
export async function createApprovalGate(config = {}) {
|
|
202
|
-
|
|
202
|
+
// Support both config.store and config.storage for storage configuration
|
|
203
|
+
const storageConfig = config.storage || config.store || { type: 'sqlite', options: {} };
|
|
204
|
+
const store = await createApprovalStore(storageConfig);
|
|
203
205
|
const notifier = new Notifier(config.notifier || {});
|
|
204
206
|
|
|
205
207
|
return new ApprovalGate({
|
|
@@ -323,9 +323,11 @@ export class IdempotencyStore {
|
|
|
323
323
|
* Create idempotency store from config
|
|
324
324
|
*/
|
|
325
325
|
export async function createIdempotencyStore(config = {}) {
|
|
326
|
+
// Support both config.storage and config.type/config.options for storage configuration
|
|
327
|
+
const storageConfig = config.storage || { type: config.type || 'sqlite', options: config.options || {} };
|
|
326
328
|
const storage = await createStorage(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
+
storageConfig.type || 'sqlite',
|
|
330
|
+
storageConfig.options || {}
|
|
329
331
|
);
|
|
330
332
|
const store = new IdempotencyStore(storage, { ttl: config.ttl });
|
|
331
333
|
await store.initialize();
|
package/src/index.js
CHANGED
package/src/llm/llm-agent.js
CHANGED
|
@@ -14,7 +14,7 @@ import { createPromptManager } from './prompt-manager.js';
|
|
|
14
14
|
import { createExecutionEngine } from '../execution/engine.js';
|
|
15
15
|
import { createApprovalGate } from '../execution/hitl/approval-gate.js';
|
|
16
16
|
import { createBudgetController } from '../execution/budget/index.js';
|
|
17
|
-
import {
|
|
17
|
+
import { createIdempotencyStore } from '../execution/idempotency.js';
|
|
18
18
|
import { v4 as uuidv4 } from 'uuid';
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -27,6 +27,8 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
27
27
|
* @property {Object} [hitl] - HITL approval config
|
|
28
28
|
* @property {Object} [budget] - Budget control config
|
|
29
29
|
* @property {Object} [idempotency] - Idempotency config
|
|
30
|
+
* @property {Object} [prompts] - Prompt manager config
|
|
31
|
+
* @property {string} [sessionId] - Session ID for conversation memory
|
|
30
32
|
*/
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -61,6 +63,9 @@ export class LLMAgent {
|
|
|
61
63
|
throw new Error(`LLM validation failed: ${valid.error}`);
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
// Get storage config
|
|
67
|
+
const storageConfig = this.config.storage || { type: 'sqlite', options: {} };
|
|
68
|
+
|
|
64
69
|
// Initialize tool registry
|
|
65
70
|
this.tools = createToolRegistry({
|
|
66
71
|
idempotencyStore: this.idempotency,
|
|
@@ -69,24 +74,39 @@ export class LLMAgent {
|
|
|
69
74
|
|
|
70
75
|
// Initialize memory systems
|
|
71
76
|
if (this.config.memory?.conversation !== false) {
|
|
72
|
-
this.memory.conversation = await createConversationMemory(
|
|
77
|
+
this.memory.conversation = await createConversationMemory({
|
|
78
|
+
...this.config.memory?.conversation,
|
|
79
|
+
storage: storageConfig
|
|
80
|
+
});
|
|
73
81
|
await this.memory.conversation.startSession(this.config.sessionId);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
if (this.config.memory?.semantic) {
|
|
77
|
-
this.memory.semantic = await createSemanticMemory(
|
|
85
|
+
this.memory.semantic = await createSemanticMemory({
|
|
86
|
+
...this.config.memory.semantic,
|
|
87
|
+
storage: storageConfig
|
|
88
|
+
});
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
if (this.config.memory?.episodic) {
|
|
81
|
-
this.memory.episodic = await createEpisodicMemory(
|
|
92
|
+
this.memory.episodic = await createEpisodicMemory({
|
|
93
|
+
...this.config.memory.episodic,
|
|
94
|
+
storage: storageConfig
|
|
95
|
+
});
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
// Initialize execution engine
|
|
85
|
-
this.execution = await createExecutionEngine(
|
|
99
|
+
this.execution = await createExecutionEngine({
|
|
100
|
+
...this.config.execution,
|
|
101
|
+
storage: storageConfig
|
|
102
|
+
});
|
|
86
103
|
|
|
87
104
|
// Initialize HITL
|
|
88
105
|
if (this.config.hitl?.enabled !== false) {
|
|
89
|
-
this.hitl = await createApprovalGate(
|
|
106
|
+
this.hitl = await createApprovalGate({
|
|
107
|
+
...this.config.hitl,
|
|
108
|
+
store: storageConfig
|
|
109
|
+
});
|
|
90
110
|
// Update tool registry with HITL
|
|
91
111
|
this.tools.approvalGate = this.hitl;
|
|
92
112
|
}
|
|
@@ -95,7 +115,10 @@ export class LLMAgent {
|
|
|
95
115
|
this.budget = createBudgetController(this.config.budget);
|
|
96
116
|
|
|
97
117
|
// Initialize idempotency
|
|
98
|
-
this.idempotency = await
|
|
118
|
+
this.idempotency = await createIdempotencyStore({
|
|
119
|
+
...this.config.idempotency,
|
|
120
|
+
...storageConfig
|
|
121
|
+
});
|
|
99
122
|
this.tools.idempotencyStore = this.idempotency;
|
|
100
123
|
|
|
101
124
|
// Initialize prompt manager
|
|
@@ -37,8 +37,9 @@ export class ConversationMemory {
|
|
|
37
37
|
async initialize() {
|
|
38
38
|
if (this.initialized) return;
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Use the new storage interface method
|
|
41
|
+
if (typeof this.storage.exec === 'function') {
|
|
42
|
+
await this.storage.exec(`
|
|
42
43
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
43
44
|
id TEXT PRIMARY KEY,
|
|
44
45
|
session_id TEXT NOT NULL,
|
|
@@ -52,11 +53,11 @@ export class ConversationMemory {
|
|
|
52
53
|
)
|
|
53
54
|
`);
|
|
54
55
|
|
|
55
|
-
await this.storage.
|
|
56
|
+
await this.storage.exec(`
|
|
56
57
|
CREATE INDEX IF NOT EXISTS idx_conversations_session ON conversations(session_id)
|
|
57
58
|
`);
|
|
58
59
|
|
|
59
|
-
await this.storage.
|
|
60
|
+
await this.storage.exec(`
|
|
60
61
|
CREATE INDEX IF NOT EXISTS idx_conversations_created ON conversations(created_at)
|
|
61
62
|
`);
|
|
62
63
|
}
|
|
@@ -86,8 +87,8 @@ export class ConversationMemory {
|
|
|
86
87
|
async loadSession(sessionId) {
|
|
87
88
|
await this.initialize();
|
|
88
89
|
|
|
89
|
-
if (this.storage.
|
|
90
|
-
const rows = await this.storage.
|
|
90
|
+
if (typeof this.storage.all === 'function') {
|
|
91
|
+
const rows = await this.storage.all(
|
|
91
92
|
'SELECT * FROM conversations WHERE session_id = ? ORDER BY created_at ASC',
|
|
92
93
|
[sessionId]
|
|
93
94
|
);
|
|
@@ -123,12 +124,12 @@ export class ConversationMemory {
|
|
|
123
124
|
this.messages.push(msg);
|
|
124
125
|
|
|
125
126
|
// Persist to storage
|
|
126
|
-
if (this.storage.
|
|
127
|
+
if (typeof this.storage.run === 'function' && this.sessionId) {
|
|
127
128
|
const tokens = this.tokenCounter
|
|
128
129
|
? await this.tokenCounter.estimateTokens([msg], { provider: 'openai', model: 'gpt-4o-mini' })
|
|
129
130
|
: 0;
|
|
130
131
|
|
|
131
|
-
await this.storage.
|
|
132
|
+
await this.storage.run(
|
|
132
133
|
`INSERT INTO conversations (id, session_id, role, content, name, tool_call_id, tool_calls, tokens, created_at)
|
|
133
134
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
134
135
|
[
|
|
@@ -229,8 +230,8 @@ export class ConversationMemory {
|
|
|
229
230
|
async clear() {
|
|
230
231
|
this.messages = [];
|
|
231
232
|
|
|
232
|
-
if (this.storage.
|
|
233
|
-
await this.storage.
|
|
233
|
+
if (typeof this.storage.run === 'function' && this.sessionId) {
|
|
234
|
+
await this.storage.run(
|
|
234
235
|
'DELETE FROM conversations WHERE session_id = ?',
|
|
235
236
|
[this.sessionId]
|
|
236
237
|
);
|
|
@@ -34,8 +34,9 @@ export class PromptManager {
|
|
|
34
34
|
async initialize() {
|
|
35
35
|
if (this.initialized) return;
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Use the new storage interface
|
|
38
|
+
if (typeof this.storage.exec === 'function') {
|
|
39
|
+
await this.storage.exec(`
|
|
39
40
|
CREATE TABLE IF NOT EXISTS prompts (
|
|
40
41
|
name TEXT NOT NULL,
|
|
41
42
|
version TEXT NOT NULL,
|
|
@@ -78,8 +79,8 @@ export class PromptManager {
|
|
|
78
79
|
this.templates.get(name).set(version, prompt);
|
|
79
80
|
|
|
80
81
|
// Persist
|
|
81
|
-
if (this.storage.
|
|
82
|
-
await this.storage.
|
|
82
|
+
if (typeof this.storage.run === 'function') {
|
|
83
|
+
await this.storage.run(
|
|
83
84
|
`INSERT OR REPLACE INTO prompts (name, version, template, variables, description, created_at, updated_at)
|
|
84
85
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
85
86
|
[name, version, template, JSON.stringify(extractedVars), description, prompt.createdAt, prompt.updatedAt]
|
|
@@ -102,18 +103,14 @@ export class PromptManager {
|
|
|
102
103
|
return versions.get(version) || null;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
for (let i = 0; i < 3; i++) {
|
|
111
|
-
if (va[i] !== vb[i]) return vb[i] - va[i];
|
|
106
|
+
// Return latest version (highest semver)
|
|
107
|
+
let latest = null;
|
|
108
|
+
for (const [ver, prompt] of versions) {
|
|
109
|
+
if (!latest || this.compareVersions(ver, latest) > 0) {
|
|
110
|
+
latest = ver;
|
|
112
111
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return versions.get(sortedVersions[0]) || null;
|
|
112
|
+
}
|
|
113
|
+
return versions.get(latest);
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
/**
|
|
@@ -123,110 +120,79 @@ export class PromptManager {
|
|
|
123
120
|
await this.initialize();
|
|
124
121
|
const versions = this.templates.get(name);
|
|
125
122
|
if (!versions) return [];
|
|
126
|
-
return Array.from(versions.values())
|
|
127
|
-
new Date(b.createdAt) - new Date(a.createdAt)
|
|
128
|
-
);
|
|
123
|
+
return Array.from(versions.values());
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
/**
|
|
132
|
-
* Render a
|
|
127
|
+
* Render a template with variables
|
|
133
128
|
*/
|
|
134
|
-
async render(name, variables, version = null) {
|
|
129
|
+
async render(name, variables = {}, version = null) {
|
|
135
130
|
const prompt = await this.get(name, version);
|
|
136
131
|
if (!prompt) {
|
|
137
132
|
throw new Error(`Prompt not found: ${name}${version ? `@${version}` : ''}`);
|
|
138
133
|
}
|
|
139
134
|
|
|
140
|
-
// Check required variables
|
|
141
|
-
for (const v of prompt.variables) {
|
|
142
|
-
if (!(v in variables)) {
|
|
143
|
-
throw new Error(`Missing required variable: ${v}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Substitute variables
|
|
148
135
|
let rendered = prompt.template;
|
|
149
136
|
for (const [key, value] of Object.entries(variables)) {
|
|
150
|
-
|
|
151
|
-
rendered = rendered.replaceAll(placeholder, String(value));
|
|
137
|
+
rendered = rendered.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
152
138
|
}
|
|
153
139
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
* Render multiple prompts (for system + user messages)
|
|
159
|
-
*/
|
|
160
|
-
async renderAll(prompts, variables) {
|
|
161
|
-
const results = [];
|
|
162
|
-
for (const { name, version, role = 'user' } of prompts) {
|
|
163
|
-
const content = await this.render(name, variables, version);
|
|
164
|
-
results.push({ role, content });
|
|
140
|
+
// Check for missing variables
|
|
141
|
+
const missingVars = prompt.variables.filter(v => !(v in variables));
|
|
142
|
+
if (missingVars.length > 0) {
|
|
143
|
+
console.warn(`Missing variables for prompt ${name}: ${missingVars.join(', ')}`);
|
|
165
144
|
}
|
|
166
|
-
|
|
145
|
+
|
|
146
|
+
return rendered;
|
|
167
147
|
}
|
|
168
148
|
|
|
169
149
|
/**
|
|
170
150
|
* Extract variables from template
|
|
171
151
|
*/
|
|
172
152
|
extractVariables(template) {
|
|
173
|
-
const matches = template.match(
|
|
153
|
+
const matches = template.match(/\{\{(\w+)\}\}/g);
|
|
174
154
|
if (!matches) return [];
|
|
175
155
|
return [...new Set(matches.map(m => m.slice(2, -2)))];
|
|
176
156
|
}
|
|
177
157
|
|
|
178
158
|
/**
|
|
179
|
-
*
|
|
159
|
+
* Compare semantic versions
|
|
180
160
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*/
|
|
189
|
-
async delete(name, version) {
|
|
190
|
-
await this.initialize();
|
|
191
|
-
|
|
192
|
-
const versions = this.templates.get(name);
|
|
193
|
-
if (!versions || !versions.has(version)) {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
versions.delete(version);
|
|
198
|
-
|
|
199
|
-
if (this.storage.db) {
|
|
200
|
-
await this.storage.db.run(
|
|
201
|
-
'DELETE FROM prompts WHERE name = ? AND version = ?',
|
|
202
|
-
[name, version]
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return true;
|
|
161
|
+
compareVersions(a, b) {
|
|
162
|
+
const parse = v => v.split('.').map(Number);
|
|
163
|
+
const [aMajor, aMinor, aPatch] = parse(a);
|
|
164
|
+
const [bMajor, bMinor, bPatch] = parse(b);
|
|
165
|
+
if (aMajor !== bMajor) return aMajor - bMajor;
|
|
166
|
+
if (aMinor !== bMinor) return aMinor - bMinor;
|
|
167
|
+
return aPatch - bPatch;
|
|
207
168
|
}
|
|
208
169
|
|
|
209
170
|
/**
|
|
210
171
|
* Load all prompts from storage
|
|
211
172
|
*/
|
|
212
|
-
async
|
|
173
|
+
async loadFromStorage() {
|
|
213
174
|
await this.initialize();
|
|
214
175
|
|
|
215
|
-
if (this.storage.
|
|
216
|
-
const rows = await this.storage.
|
|
176
|
+
if (typeof this.storage.all === 'function') {
|
|
177
|
+
const rows = await this.storage.all(
|
|
178
|
+
'SELECT * FROM prompts ORDER BY name, version'
|
|
179
|
+
);
|
|
180
|
+
|
|
217
181
|
for (const row of rows) {
|
|
218
|
-
|
|
219
|
-
this.templates.set(row.name, new Map());
|
|
220
|
-
}
|
|
221
|
-
this.templates.get(row.name).set(row.version, {
|
|
182
|
+
const prompt = {
|
|
222
183
|
name: row.name,
|
|
223
184
|
version: row.version,
|
|
224
185
|
template: row.template,
|
|
225
|
-
variables: JSON.parse(row.variables
|
|
186
|
+
variables: JSON.parse(row.variables),
|
|
226
187
|
description: row.description,
|
|
227
188
|
createdAt: row.created_at,
|
|
228
189
|
updatedAt: row.updated_at,
|
|
229
|
-
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (!this.templates.has(prompt.name)) {
|
|
193
|
+
this.templates.set(prompt.name, new Map());
|
|
194
|
+
}
|
|
195
|
+
this.templates.get(prompt.name).set(prompt.version, prompt);
|
|
230
196
|
}
|
|
231
197
|
}
|
|
232
198
|
}
|
|
@@ -237,79 +203,31 @@ export class PromptManager {
|
|
|
237
203
|
*/
|
|
238
204
|
export const BUILTIN_PROMPTS = {
|
|
239
205
|
'system:react': {
|
|
240
|
-
|
|
206
|
+
name: 'system:react',
|
|
207
|
+
version: '1.0.0',
|
|
208
|
+
template: `You are an AI assistant that uses the ReAct (Reasoning + Acting) pattern.
|
|
241
209
|
|
|
242
210
|
You have access to the following tools:
|
|
243
211
|
{{tools}}
|
|
244
212
|
|
|
245
|
-
|
|
213
|
+
Your task is: {{task}}
|
|
214
|
+
|
|
215
|
+
Use the following format:
|
|
216
|
+
|
|
246
217
|
THOUGHT: Your reasoning about what to do next
|
|
247
|
-
ACTION: The tool
|
|
218
|
+
ACTION: The tool to use (must be one of the available tools)
|
|
248
219
|
ACTION_INPUT: The parameters for the tool
|
|
249
220
|
|
|
250
|
-
|
|
251
|
-
OBSERVATION: The result
|
|
252
|
-
|
|
253
|
-
Continue this pattern until you can provide the final answer.
|
|
221
|
+
When you have the final answer, respond directly without THOUGHT/ACTION format.
|
|
254
222
|
|
|
255
|
-
|
|
223
|
+
Begin!`,
|
|
256
224
|
variables: ['tools', 'task'],
|
|
257
|
-
description: '
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
'system:planner': {
|
|
261
|
-
template: `You are a planning agent. Break down the task into a sequence of steps.
|
|
262
|
-
|
|
263
|
-
Task: {{task}}
|
|
264
|
-
|
|
265
|
-
Available tools: {{tools}}
|
|
266
|
-
|
|
267
|
-
Create a plan with numbered steps. Each step should specify:
|
|
268
|
-
1. What tool to use (if any)
|
|
269
|
-
2. What parameters to pass
|
|
270
|
-
3. What you expect to learn or achieve
|
|
271
|
-
|
|
272
|
-
Output as JSON:
|
|
273
|
-
{
|
|
274
|
-
"steps": [
|
|
275
|
-
{"step": 1, "tool": "tool_name", "params": {}, "description": "..."}
|
|
276
|
-
]
|
|
277
|
-
}`,
|
|
278
|
-
variables: ['task', 'tools'],
|
|
279
|
-
description: 'Planning agent prompt',
|
|
280
|
-
},
|
|
281
|
-
|
|
282
|
-
'system:code-reviewer': {
|
|
283
|
-
template: `You are an expert code reviewer. Analyze the provided code for:
|
|
284
|
-
- Bugs and logic errors
|
|
285
|
-
- Security vulnerabilities
|
|
286
|
-
- Performance issues
|
|
287
|
-
- Code style and best practices
|
|
288
|
-
- Test coverage gaps
|
|
289
|
-
|
|
290
|
-
Code to review:
|
|
291
|
-
{{code}}
|
|
292
|
-
|
|
293
|
-
Context: {{context}}
|
|
294
|
-
|
|
295
|
-
Provide your review in this format:
|
|
296
|
-
## Summary
|
|
297
|
-
Brief overall assessment
|
|
298
|
-
|
|
299
|
-
## Issues Found
|
|
300
|
-
- [Severity] File:Line - Description
|
|
301
|
-
|
|
302
|
-
## Suggestions
|
|
303
|
-
- Improvement suggestions
|
|
304
|
-
|
|
305
|
-
## Approved: true/false`,
|
|
306
|
-
variables: ['code', 'context'],
|
|
307
|
-
description: 'Code review prompt',
|
|
225
|
+
description: 'System prompt for ReAct agent',
|
|
308
226
|
},
|
|
309
227
|
};
|
|
310
228
|
|
|
311
229
|
/**
|
|
312
|
-
* Create prompt manager
|
|
230
|
+
* Create prompt manager from config
|
|
313
231
|
*/
|
|
314
232
|
export async function createPromptManager(config = {}) {
|
|
315
233
|
const storage = await createStorage(
|
|
@@ -323,6 +241,7 @@ export async function createPromptManager(config = {}) {
|
|
|
323
241
|
// Register built-in prompts
|
|
324
242
|
for (const [name, prompt] of Object.entries(BUILTIN_PROMPTS)) {
|
|
325
243
|
await manager.register(name, prompt.template, {
|
|
244
|
+
version: prompt.version,
|
|
326
245
|
variables: prompt.variables,
|
|
327
246
|
description: prompt.description,
|
|
328
247
|
});
|
package/src/server/index.js
CHANGED
|
@@ -280,7 +280,7 @@ export function createApp(config = {}) {
|
|
|
280
280
|
connectedClients.add(socket.id);
|
|
281
281
|
console.log(`Client connected: ${socket.id} (total: ${connectedClients.size})`);
|
|
282
282
|
|
|
283
|
-
// Send current state
|
|
283
|
+
// Send current state
|
|
284
284
|
Promise.all(
|
|
285
285
|
Array.from(activeRuns.entries()).map(async ([runId, run]) => {
|
|
286
286
|
const approvals = await run.hitl.getPendingApprovals(50);
|
|
@@ -316,12 +316,7 @@ export function createApp(config = {}) {
|
|
|
316
316
|
});
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
function broadcast(event, data) {
|
|
321
|
-
io.emit(event, data);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return { app, httpServer, io, broadcast, activeRuns };
|
|
319
|
+
return { app, httpServer, io, activeRuns };
|
|
325
320
|
}
|
|
326
321
|
|
|
327
322
|
/**
|
|
@@ -362,7 +357,7 @@ async function runAgent(runId, task, hitlGate, budgetCtrl, workflowId, io) {
|
|
|
362
357
|
io.to(`run:${runId}`).emit('thought:new', run.thoughts[run.thoughts.length - 1]);
|
|
363
358
|
|
|
364
359
|
io.to(`run:${runId}`).emit('run:completed', { runId, result });
|
|
365
|
-
|
|
360
|
+
io.emit('run:updated', { runId, status: run.status, result });
|
|
366
361
|
|
|
367
362
|
} catch (error) {
|
|
368
363
|
run.status = 'error';
|
|
@@ -377,7 +372,7 @@ async function runAgent(runId, task, hitlGate, budgetCtrl, workflowId, io) {
|
|
|
377
372
|
io.to(`run:${runId}`).emit('thought:new', run.thoughts[run.thoughts.length - 1]);
|
|
378
373
|
|
|
379
374
|
io.to(`run:${runId}`).emit('run:error', { runId, error: error.message });
|
|
380
|
-
|
|
375
|
+
io.emit('run:updated', { runId, status: 'error', error: error.message });
|
|
381
376
|
}
|
|
382
377
|
}
|
|
383
378
|
|
|
@@ -385,7 +380,7 @@ async function runAgent(runId, task, hitlGate, budgetCtrl, workflowId, io) {
|
|
|
385
380
|
* Start the server
|
|
386
381
|
*/
|
|
387
382
|
export async function startServer(config = {}) {
|
|
388
|
-
const { app, httpServer, io,
|
|
383
|
+
const { app, httpServer, io, activeRuns: runs } = createApp(config);
|
|
389
384
|
|
|
390
385
|
const port = config.port || process.env.LSJI_SERVER_PORT || 3456;
|
|
391
386
|
const host = config.host || '0.0.0.0';
|
|
@@ -394,7 +389,7 @@ export async function startServer(config = {}) {
|
|
|
394
389
|
httpServer.listen(port, host, () => {
|
|
395
390
|
console.log(`LSJI Server running at http://${host}:${port}`);
|
|
396
391
|
console.log(`WebSocket ready for connections`);
|
|
397
|
-
resolve({ app, httpServer, io,
|
|
392
|
+
resolve({ app, httpServer, io, activeRuns: runs, port, host });
|
|
398
393
|
});
|
|
399
394
|
});
|
|
400
395
|
}
|
|
@@ -130,4 +130,35 @@ export class BetterSqliteStorage extends Storage {
|
|
|
130
130
|
`);
|
|
131
131
|
return stmt.all();
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
// ===== Generic SQL methods =====
|
|
135
|
+
|
|
136
|
+
async all(sql, params = []) {
|
|
137
|
+
const stmt = this.db.prepare(sql);
|
|
138
|
+
return stmt.all(...params);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async get(sql, params = []) {
|
|
142
|
+
const stmt = this.db.prepare(sql);
|
|
143
|
+
return stmt.get(...params);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async run(sql, params = []) {
|
|
147
|
+
const stmt = this.db.prepare(sql);
|
|
148
|
+
const result = stmt.run(...params);
|
|
149
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async exec(sql) {
|
|
153
|
+
this.db.exec(sql);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
prepare(sql) {
|
|
157
|
+
const stmt = this.db.prepare(sql);
|
|
158
|
+
return {
|
|
159
|
+
run: (...params) => stmt.run(...params),
|
|
160
|
+
get: (...params) => stmt.get(...params),
|
|
161
|
+
all: (...params) => stmt.all(...params),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
133
164
|
}
|
package/src/storage/index.js
CHANGED
|
@@ -114,6 +114,56 @@ export class Storage {
|
|
|
114
114
|
async getPerformanceStats() {
|
|
115
115
|
throw new Error('getPerformanceStats() must be implemented');
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
// ===== Generic SQL methods for custom queries =====
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Execute a SELECT query returning all rows
|
|
122
|
+
* @param {string} sql - SQL query
|
|
123
|
+
* @param {Array} params - Query parameters
|
|
124
|
+
* @returns {Promise<Array<Object>>}
|
|
125
|
+
*/
|
|
126
|
+
async all(sql, params = []) {
|
|
127
|
+
throw new Error('all() must be implemented');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execute a SELECT query returning first row
|
|
132
|
+
* @param {string} sql - SQL query
|
|
133
|
+
* @param {Array} params - Query parameters
|
|
134
|
+
* @returns {Promise<Object|null>}
|
|
135
|
+
*/
|
|
136
|
+
async get(sql, params = []) {
|
|
137
|
+
throw new Error('get() must be implemented');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Execute an INSERT/UPDATE/DELETE query
|
|
142
|
+
* @param {string} sql - SQL query
|
|
143
|
+
* @param {Array} params - Query parameters
|
|
144
|
+
* @returns {Promise<{changes: number, lastInsertRowid: number|bigint}>}
|
|
145
|
+
*/
|
|
146
|
+
async run(sql, params = []) {
|
|
147
|
+
throw new Error('run() must be implemented');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Execute multiple SQL statements
|
|
152
|
+
* @param {string} sql - SQL statements
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
async exec(sql) {
|
|
156
|
+
throw new Error('exec() must be implemented');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Prepare a statement for repeated execution
|
|
161
|
+
* @param {string} sql - SQL query
|
|
162
|
+
* @returns {Object} Prepared statement with run(), get(), all() methods
|
|
163
|
+
*/
|
|
164
|
+
prepare(sql) {
|
|
165
|
+
throw new Error('prepare() must be implemented');
|
|
166
|
+
}
|
|
117
167
|
}
|
|
118
168
|
|
|
119
169
|
/**
|
package/src/storage/memory.js
CHANGED
|
@@ -17,6 +17,7 @@ export class MemoryStorage extends Storage {
|
|
|
17
17
|
this.settings = new Map();
|
|
18
18
|
this.battleHistory = [];
|
|
19
19
|
this.qTable = new Map(); // key: "state:action" -> q_value
|
|
20
|
+
this.conversations = []; // For conversation memory
|
|
20
21
|
this.initialized = false;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -93,6 +94,193 @@ export class MemoryStorage extends Storage {
|
|
|
93
94
|
this.settings.clear();
|
|
94
95
|
this.battleHistory = [];
|
|
95
96
|
this.qTable.clear();
|
|
97
|
+
this.conversations = [];
|
|
96
98
|
this.settings.set('is_active', '1');
|
|
97
99
|
}
|
|
100
|
+
|
|
101
|
+
// ===== Generic SQL methods (in-memory implementation) =====
|
|
102
|
+
|
|
103
|
+
async all(sql, params = []) {
|
|
104
|
+
// Simple in-memory SQL-like query parser for basic SELECT queries
|
|
105
|
+
// This is a simplified implementation for testing
|
|
106
|
+
const lowerSql = sql.toLowerCase().trim();
|
|
107
|
+
|
|
108
|
+
if (lowerSql.startsWith('select')) {
|
|
109
|
+
if (lowerSql.includes('from conversations')) {
|
|
110
|
+
let results = [...this.conversations];
|
|
111
|
+
|
|
112
|
+
// Simple WHERE clause handling for session_id
|
|
113
|
+
const whereMatch = lowerSql.match(/where\s+session_id\s*=\s*\?/i);
|
|
114
|
+
if (whereMatch && params.length > 0) {
|
|
115
|
+
const sessionId = params[0];
|
|
116
|
+
results = results.filter(r => r.session_id === sessionId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ORDER BY created_at ASC
|
|
120
|
+
results.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
121
|
+
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (lowerSql.includes('from idempotency_keys')) {
|
|
126
|
+
// For idempotency store
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (lowerSql.includes('from checkpoints')) {
|
|
131
|
+
// For execution engine
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (lowerSql.includes('from approvals')) {
|
|
136
|
+
// For HITL
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async get(sql, params = []) {
|
|
145
|
+
const results = await this.all(sql, params);
|
|
146
|
+
return results[0] || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async run(sql, params = []) {
|
|
150
|
+
const lowerSql = sql.toLowerCase().trim();
|
|
151
|
+
|
|
152
|
+
if (lowerSql.startsWith('insert')) {
|
|
153
|
+
if (lowerSql.includes('into conversations')) {
|
|
154
|
+
// INSERT INTO conversations (id, session_id, role, content, name, tool_call_id, tool_calls, tokens, created_at)
|
|
155
|
+
const id = params[0];
|
|
156
|
+
const sessionId = params[1];
|
|
157
|
+
const role = params[2];
|
|
158
|
+
const content = params[3];
|
|
159
|
+
const name = params[4];
|
|
160
|
+
const toolCallId = params[5];
|
|
161
|
+
const toolCalls = params[6];
|
|
162
|
+
const tokens = params[7];
|
|
163
|
+
const createdAt = params[8];
|
|
164
|
+
|
|
165
|
+
this.conversations.push({
|
|
166
|
+
id,
|
|
167
|
+
session_id: sessionId,
|
|
168
|
+
role,
|
|
169
|
+
content,
|
|
170
|
+
name,
|
|
171
|
+
tool_call_id: toolCallId,
|
|
172
|
+
tool_calls: toolCalls,
|
|
173
|
+
tokens,
|
|
174
|
+
created_at: createdAt,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (lowerSql.includes('into idempotency_keys')) {
|
|
179
|
+
// For idempotency store
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (lowerSql.includes('into checkpoints')) {
|
|
183
|
+
// For execution engine
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (lowerSql.includes('into approvals')) {
|
|
187
|
+
// For HITL
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (lowerSql.includes('into settings')) {
|
|
191
|
+
// Handled by setSetting
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (lowerSql.includes('into battle_history')) {
|
|
195
|
+
// Handled by addBattle
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (lowerSql.includes('into q_table')) {
|
|
199
|
+
// Handled by updateQ
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { changes: 1, lastInsertRowid: Date.now() };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (lowerSql.startsWith('update')) {
|
|
206
|
+
if (lowerSql.includes('idempotency_keys')) {
|
|
207
|
+
// For idempotency store
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (lowerSql.includes('approvals')) {
|
|
211
|
+
// For HITL
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { changes: 1, lastInsertRowid: 0 };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (lowerSql.startsWith('delete')) {
|
|
218
|
+
if (lowerSql.includes('from conversations')) {
|
|
219
|
+
if (params[0]) {
|
|
220
|
+
this.conversations = this.conversations.filter(c => c.session_id !== params[0]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (lowerSql.includes('from idempotency_keys')) {
|
|
225
|
+
// For idempotency store
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { changes: 1, lastInsertRowid: 0 };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { changes: 0, lastInsertRowid: 0 };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async exec(sql) {
|
|
235
|
+
// No-op for memory storage
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
prepare(sql) {
|
|
239
|
+
const self = this;
|
|
240
|
+
return {
|
|
241
|
+
run: async (...params) => (await self.run(sql, params)),
|
|
242
|
+
get: async (...params) => (await self.get(sql, params)),
|
|
243
|
+
all: async (...params) => (await self.all(sql, params)),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Helper methods for conversation memory
|
|
248
|
+
_addConversation(message) {
|
|
249
|
+
this.conversations.push(message);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_getConversations(sessionId) {
|
|
253
|
+
return this.conversations.filter(c => c.session_id === sessionId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_clearConversations(sessionId) {
|
|
257
|
+
this.conversations = this.conversations.filter(c => c.session_id !== sessionId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// For idempotency store
|
|
261
|
+
_getIdempotencyKeys() {
|
|
262
|
+
return this.idempotencyKeys || new Map();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_setIdempotencyKeys(keys) {
|
|
266
|
+
this.idempotencyKeys = keys;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// For checkpoints
|
|
270
|
+
_getCheckpoints() {
|
|
271
|
+
return this.checkpoints || new Map();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_setCheckpoints(checkpoints) {
|
|
275
|
+
this.checkpoints = checkpoints;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// For approvals
|
|
279
|
+
_getApprovals() {
|
|
280
|
+
return this.approvals || new Map();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_setApprovals(approvals) {
|
|
284
|
+
this.approvals = approvals;
|
|
285
|
+
}
|
|
98
286
|
}
|
package/src/storage/sqlite.js
CHANGED
|
@@ -120,4 +120,35 @@ export class SqliteStorage extends Storage {
|
|
|
120
120
|
`);
|
|
121
121
|
return stmt.all();
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
// ===== Generic SQL methods =====
|
|
125
|
+
|
|
126
|
+
async all(sql, params = []) {
|
|
127
|
+
const stmt = this.db.prepare(sql);
|
|
128
|
+
return stmt.all(...params);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async get(sql, params = []) {
|
|
132
|
+
const stmt = this.db.prepare(sql);
|
|
133
|
+
return stmt.get(...params);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async run(sql, params = []) {
|
|
137
|
+
const stmt = this.db.prepare(sql);
|
|
138
|
+
const result = stmt.run(...params);
|
|
139
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async exec(sql) {
|
|
143
|
+
this.db.exec(sql);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
prepare(sql) {
|
|
147
|
+
const stmt = this.db.prepare(sql);
|
|
148
|
+
return {
|
|
149
|
+
run: (...params) => stmt.run(...params),
|
|
150
|
+
get: (...params) => stmt.get(...params),
|
|
151
|
+
all: (...params) => stmt.all(...params),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
123
154
|
}
|