@game_ryo/lsji 0.1.0 → 0.3.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 (75) hide show
  1. package/package.json +15 -7
  2. package/src/cli.js +395 -62
  3. package/src/execution/budget/circuit-breaker.js +245 -0
  4. package/src/execution/budget/cost-tracker.js +387 -0
  5. package/src/execution/budget/index.js +63 -0
  6. package/src/execution/budget/token-counter.js +159 -0
  7. package/src/execution/engine.js +428 -0
  8. package/src/execution/hitl/approval-gate.js +210 -0
  9. package/src/execution/hitl/index.js +12 -0
  10. package/src/execution/hitl/notifier.js +151 -0
  11. package/src/execution/hitl/store.js +311 -0
  12. package/src/execution/idempotency.js +312 -0
  13. package/src/execution/index.js +14 -0
  14. package/src/index.js +80 -4
  15. package/src/llm/index.js +21 -0
  16. package/src/llm/llm-agent.js +357 -0
  17. package/src/llm/memory/conversation.js +271 -0
  18. package/src/llm/memory/episodic.js +312 -0
  19. package/src/llm/memory/index.js +12 -0
  20. package/src/llm/memory/semantic.js +324 -0
  21. package/src/llm/plugins/index.js +202 -0
  22. package/src/llm/prompt-manager.js +332 -0
  23. package/src/llm/providers/anthropic.js +250 -0
  24. package/src/llm/providers/base.js +116 -0
  25. package/src/llm/providers/local.js +163 -0
  26. package/src/llm/providers/openai.js +212 -0
  27. package/src/llm/tools/registry.js +342 -0
  28. package/src/server/index.js +416 -0
  29. package/src/server/ui/index.html +16 -0
  30. package/src/server/ui/package.json +19 -0
  31. package/src/server/ui/src/main.jsx +10 -0
  32. package/src/server/ui/src/styles.css +260 -0
  33. package/src/server/ui/vite.config.js +27 -0
  34. package/docs/README.md +0 -43
  35. package/docs/blog/2019-05-28-first-blog-post.mdx +0 -12
  36. package/docs/blog/2019-05-29-long-blog-post.mdx +0 -44
  37. package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
  38. package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  39. package/docs/blog/2021-08-26-welcome/index.mdx +0 -29
  40. package/docs/blog/authors.yml +0 -25
  41. package/docs/blog/tags.yml +0 -19
  42. package/docs/docs/api/agent.md +0 -151
  43. package/docs/docs/api/env.md +0 -133
  44. package/docs/docs/api/environments.md +0 -102
  45. package/docs/docs/api/qlearning.md +0 -138
  46. package/docs/docs/api/storage.md +0 -168
  47. package/docs/docs/architecture.md +0 -155
  48. package/docs/docs/cli.md +0 -210
  49. package/docs/docs/contributing.md +0 -162
  50. package/docs/docs/core-concepts.md +0 -152
  51. package/docs/docs/examples/advanced-training.md +0 -244
  52. package/docs/docs/examples/custom-environment.md +0 -198
  53. package/docs/docs/examples/custom-storage.md +0 -251
  54. package/docs/docs/getting-started.md +0 -91
  55. package/docs/docusaurus.config.ts +0 -149
  56. package/docs/package-lock.json +0 -19522
  57. package/docs/package.json +0 -49
  58. package/docs/sidebars.ts +0 -33
  59. package/docs/src/components/HomepageFeatures/index.tsx +0 -71
  60. package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  61. package/docs/src/css/custom.css +0 -79
  62. package/docs/src/pages/index.module.css +0 -23
  63. package/docs/src/pages/index.tsx +0 -44
  64. package/docs/src/pages/markdown-page.mdx +0 -7
  65. package/docs/static/.nojekyll +0 -0
  66. package/docs/static/img/docusaurus-social-card.jpg +0 -0
  67. package/docs/static/img/docusaurus.png +0 -0
  68. package/docs/static/img/favicon.ico +0 -0
  69. package/docs/static/img/logo.png +0 -0
  70. package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  71. package/docs/static/img/undraw_docusaurus_react.svg +0 -170
  72. package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  73. package/docs/tsconfig.json +0 -12
  74. package/legacy/worker.js +0 -166
  75. package/legacy/wrangler.toml +0 -11
@@ -1,244 +0,0 @@
1
- ---
2
- title: Advanced Training
3
- description: Custom training patterns and techniques
4
- ---
5
-
6
- # Advanced Training
7
-
8
- Learn advanced training techniques for better agent performance.
9
-
10
- ## Custom Action Selectors
11
-
12
- The `train()` method accepts an `actionSelector` function for custom training patterns.
13
-
14
- ```typescript
15
- const result = await agent.train({
16
- episodes: 1000,
17
- actionSelector: (episode, lastAction) => {
18
- // Your custom logic here
19
- return action;
20
- }
21
- });
22
- ```
23
-
24
- ### Epsilon-Greedy with Decay
25
-
26
- ```typescript
27
- let epsilon = 1.0;
28
- const minEpsilon = 0.01;
29
- const decayRate = 0.9995;
30
-
31
- const result = await agent.train({
32
- episodes: 10000,
33
- actionSelector: (episode, lastAction) => {
34
- epsilon = Math.max(minEpsilon, epsilon * decayRate);
35
-
36
- if (Math.random() < epsilon) {
37
- return Math.floor(Math.random() * 3); // Explore
38
- }
39
-
40
- // Exploit: use agent's Q-learning
41
- const state = await agent.env.getState();
42
- return agent.qlearning.act(state, 3);
43
- }
44
- });
45
- ```
46
-
47
- ### Curriculum Learning
48
-
49
- Start with easy opponents, progress to harder ones.
50
-
51
- ```typescript
52
- const opponents = ['always_rock', 'sequential', 'counter', 'random'];
53
- const episodesPerStage = 250;
54
-
55
- for (const opponent of opponents) {
56
- const env = new RockPaperScissorsEnv({ opponent });
57
- agent.setEnvironment(env);
58
-
59
- console.log(`Training against ${opponent}...`);
60
- await agent.train({ episodes: episodesPerStage });
61
-
62
- const status = await agent.status();
63
- console.log(`Win rate: ${status.performance.find(p => p.mode === 'train')?.win_rate}%`);
64
- }
65
- ```
66
-
67
- ### Self-Play Training
68
-
69
- Train agent against itself.
70
-
71
- ```typescript
72
- // Create two agents sharing the same Q-table
73
- const storage = await createStorage('sqlite', { path: './selfplay.db' });
74
- const qlearning = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 0.1, storage });
75
-
76
- const env1 = new RockPaperScissorsEnv({ opponent: 'random' });
77
- const env2 = new RockPaperScissorsEnv({ opponent: 'random' });
78
-
79
- const agent1 = new Agent({ qlearning, storage, env: env1 });
80
- const agent2 = new Agent({ qlearning, storage, env: env2 });
81
-
82
- // Alternate training
83
- for (let i = 0; i < 100; i++) {
84
- await agent1.train({ episodes: 50 });
85
- await agent2.train({ episodes: 50 });
86
-
87
- if (i % 10 === 0) {
88
- const status = await agent1.status();
89
- console.log(`Iteration ${i}: ${status.performance[0].win_rate}% win rate`);
90
- }
91
- }
92
- ```
93
-
94
- ## Hyperparameter Tuning
95
-
96
- ### Grid Search
97
-
98
- ```typescript
99
- const configs = [
100
- { alpha: 0.05, gamma: 0.9, epsilon: 0.1 },
101
- { alpha: 0.1, gamma: 0.9, epsilon: 0.1 },
102
- { alpha: 0.2, gamma: 0.9, epsilon: 0.1 },
103
- { alpha: 0.1, gamma: 0.95, epsilon: 0.1 },
104
- { alpha: 0.1, gamma: 0.9, epsilon: 0.2 },
105
- ];
106
-
107
- for (const config of configs) {
108
- const storage = await createStorage('memory');
109
- const qlearning = new QLearning({ ...config, storage });
110
- const env = new RockPaperScissorsEnv({ opponent: 'random' });
111
- const agent = new Agent({ qlearning, storage, env });
112
-
113
- await agent.train({ episodes: 2000 });
114
- const status = await agent.status();
115
- const winRate = status.performance.find(p => p.mode === 'train')?.win_rate || 0;
116
-
117
- console.log(`${JSON.stringify(config)} => ${winRate}%`);
118
- await storage.close();
119
- }
120
- ```
121
-
122
- ### Bayesian Optimization
123
-
124
- Use libraries like `bayes-opt` for efficient hyperparameter search.
125
-
126
- ## Evaluation Techniques
127
-
128
- ### Fixed Opponent Evaluation
129
-
130
- ```typescript
131
- async function evaluate(agent, opponent, games = 100) {
132
- const env = new RockPaperScissorsEnv({ opponent });
133
- agent.setEnvironment(env);
134
-
135
- let wins = 0, losses = 0, draws = 0;
136
-
137
- for (let i = 0; i < games; i++) {
138
- const result = await agent.play(Math.floor(Math.random() * 3));
139
- if (result.reward > 0) wins++;
140
- else if (result.reward < 0) losses++;
141
- else draws++;
142
- }
143
-
144
- return { wins, losses, draws, winRate: wins / games };
145
- }
146
-
147
- const agents = {
148
- random: await evaluate(agent, 'random'),
149
- alwaysRock: await evaluate(agent, 'always_rock'),
150
- counter: await evaluate(agent, 'counter'),
151
- sequential: await evaluate(agent, 'sequential'),
152
- };
153
-
154
- console.table(agents);
155
- ```
156
-
157
- ### Cross-Validation
158
-
159
- ```typescript
160
- async function crossValidate(config, folds = 5, episodesPerFold = 1000) {
161
- const results = [];
162
-
163
- for (let fold = 0; fold < folds; fold++) {
164
- const storage = await createStorage('memory');
165
- const qlearning = new QLearning({ ...config, storage });
166
- const env = new RockPaperScissorsEnv({ opponent: 'random' });
167
- const agent = new Agent({ qlearning, storage, env });
168
-
169
- await agent.train({ episodes: episodesPerFold });
170
- const evalResult = await evaluate(agent, 'random', 200);
171
- results.push(evalResult.winRate);
172
-
173
- await storage.close();
174
- }
175
-
176
- const mean = results.reduce((a, b) => a + b, 0) / results.length;
177
- const std = Math.sqrt(results.reduce((a, b) => a + (b - mean) ** 2, 0) / results.length);
178
-
179
- return { mean, std, results };
180
- }
181
- ```
182
-
183
- ## Checkpointing and Resuming
184
-
185
- ```typescript
186
- // Save Q-table periodically
187
- async function trainWithCheckpoints(agent, episodes, checkpointEvery = 100) {
188
- for (let i = 0; i < episodes; i += checkpointEvery) {
189
- const batch = Math.min(checkpointEvery, episodes - i);
190
- await agent.train({ episodes: batch });
191
-
192
- // Q-table automatically persisted to storage
193
- const status = await agent.status();
194
- console.log(`Checkpoint ${i + batch}: ${status.aiBrain.length} Q-values`);
195
- }
196
- }
197
-
198
- // Resume from existing Q-table
199
- const storage = await createStorage('sqlite', { path: './existing.db' });
200
- const qlearning = new QLearning({ alpha: 0.1, gamma: 0.9, epsilon: 0.1, storage });
201
- // Q-table loads automatically on first use
202
- ```
203
-
204
- ## Distributed Training
205
-
206
- Run multiple training processes with shared storage.
207
-
208
- ```bash
209
- # Terminal 1
210
- lsji train --episodes 500 --db-path ./shared.db --storage sqlite
211
-
212
- # Terminal 2 (same database)
213
- lsji train --episodes 500 --db-path ./shared.db --storage sqlite
214
-
215
- # Terminal 3
216
- lsji train --episodes 500 --db-path ./shared.db --storage sqlite
217
- ```
218
-
219
- All processes read/write to the same SQLite database, enabling parallel training.
220
-
221
- ## Monitoring Training Progress
222
-
223
- ```typescript
224
- async function trainWithLogging(agent, episodes) {
225
- const history = [];
226
-
227
- for (let i = 0; i < episodes; i += 100) {
228
- await agent.train({ episodes: 100 });
229
-
230
- const status = await agent.status();
231
- const trainStat = status.performance.find(p => p.mode === 'train');
232
-
233
- history.push({
234
- episode: i + 100,
235
- winRate: trainStat?.win_rate || 0,
236
- qTableSize: status.aiBrain.length
237
- });
238
-
239
- console.log(`Episode ${i + 100}: ${history[history.length - 1].winRate}% win rate`);
240
- }
241
-
242
- return history;
243
- }
244
- ```
@@ -1,198 +0,0 @@
1
- ---
2
- title: Custom Environment
3
- description: Build your own RL environment
4
- ---
5
-
6
- # Custom Environment Example
7
-
8
- This guide shows how to create a custom environment by extending the `Env` base class.
9
-
10
- ## Grid World Environment
11
-
12
- A simple 1D grid where the agent learns to move right to reach the goal.
13
-
14
- ```typescript
15
- import { Env } from 'lsji';
16
-
17
- class GridWorldEnv extends Env {
18
- constructor(gridSize = 10) {
19
- super();
20
- this.gridSize = gridSize;
21
- this.position = 0;
22
- }
23
-
24
- getState(): string {
25
- return String(this.position);
26
- }
27
-
28
- async step(action: number): Promise<StepResult> {
29
- // Actions: 0 = left, 1 = right
30
- if (action === 0) {
31
- this.position = Math.max(0, this.position - 1);
32
- } else if (action === 1) {
33
- this.position = Math.min(this.gridSize - 1, this.position + 1);
34
- }
35
-
36
- const done = this.position === this.gridSize - 1;
37
- const reward = done ? 1 : -0.01; // Small penalty for each step
38
-
39
- return {
40
- state: String(this.position),
41
- reward,
42
- done,
43
- info: { position: this.position }
44
- };
45
- }
46
-
47
- actionSize(): number {
48
- return 2; // Left, Right
49
- }
50
-
51
- async reset(): Promise<string> {
52
- this.position = 0;
53
- return '0';
54
- }
55
-
56
- render(): string {
57
- const bar = ' '.repeat(this.gridSize);
58
- const chars = bar.split('');
59
- chars[this.position] = 'A';
60
- chars[this.gridSize - 1] = 'G';
61
- return `[${chars.join('')}]`;
62
- }
63
- }
64
- ```
65
-
66
- ## Using the Custom Environment
67
-
68
- ```typescript
69
- import { Agent, QLearning, createStorage } from 'lsji';
70
- import { GridWorldEnv } from './grid-world';
71
-
72
- async function main() {
73
- const storage = await createStorage('sqlite', { path: './gridworld.db' });
74
-
75
- const qlearning = new QLearning({
76
- alpha: 0.1,
77
- gamma: 0.9,
78
- epsilon: 0.1,
79
- storage
80
- });
81
-
82
- const env = new GridWorldEnv(10);
83
- const agent = new Agent({ qlearning, storage, env });
84
-
85
- console.log('Training...');
86
- await agent.train({ episodes: 5000 });
87
-
88
- console.log('Testing...');
89
- await agent.play(); // Single step
90
-
91
- const status = await agent.status();
92
- console.log('Q-table:', status.aiBrain);
93
-
94
- await storage.close();
95
- }
96
-
97
- main().catch(console.error);
98
- ```
99
-
100
- ## Multi-State Environment
101
-
102
- For environments with multiple state variables, use `StateEncoder`:
103
-
104
- ```typescript
105
- import { Env, StateEncoder } from 'lsji';
106
-
107
- interface GameState {
108
- playerHP: number;
109
- enemyHP: number;
110
- hasPotion: boolean;
111
- }
112
-
113
- class BattleEnv extends Env {
114
- state: GameState = { playerHP: 100, enemyHP: 50, hasPotion: true };
115
-
116
- getState(): string {
117
- return StateEncoder.encode(this.state);
118
- }
119
-
120
- async step(action: number): Promise<StepResult> {
121
- // 0 = attack, 1 = heal, 2 = defend
122
- let reward = 0;
123
- let done = false;
124
-
125
- if (action === 0) { // Attack
126
- this.state.enemyHP -= 10;
127
- reward = this.state.enemyHP <= 0 ? 10 : -1;
128
- done = this.state.enemyHP <= 0;
129
- } else if (action === 1) { // Heal
130
- if (this.state.hasPotion) {
131
- this.state.playerHP = Math.min(100, this.state.playerHP + 30);
132
- this.state.hasPotion = false;
133
- reward = -1;
134
- } else {
135
- reward = -5; // No potion penalty
136
- }
137
- } else if (action === 2) { // Defend
138
- reward = -0.5;
139
- }
140
-
141
- // Enemy counter-attack
142
- if (!done) {
143
- this.state.playerHP -= 5;
144
- if (this.state.playerHP <= 0) {
145
- reward = -10;
146
- done = true;
147
- }
148
- }
149
-
150
- return {
151
- state: StateEncoder.encode(this.state),
152
- reward,
153
- done,
154
- info: { ...this.state }
155
- };
156
- }
157
-
158
- actionSize(): number {
159
- return 3;
160
- }
161
-
162
- async reset(): Promise<string> {
163
- this.state = { playerHP: 100, enemyHP: 50, hasPotion: true };
164
- return StateEncoder.encode(this.state);
165
- }
166
- }
167
- ```
168
-
169
- ## Key Points
170
-
171
- 1. **State as string** — Use `StateEncoder.encode()` for complex states
172
- 2. **Reward design** — Shape rewards to guide learning (dense vs sparse)
173
- 3. **Action space** — Keep small for tabular Q-learning
174
- 4. **Episode termination** — Always implement `done` condition
175
- 5. **Reset** — Must restore initial state completely
176
-
177
- ## Testing Your Environment
178
-
179
- ```typescript
180
- async function testEnv() {
181
- const env = new GridWorldEnv(5);
182
-
183
- console.log('Initial:', env.getState());
184
- console.log(env.render());
185
-
186
- for (let i = 0; i < 10; i++) {
187
- const result = await env.step(1); // Always move right
188
- console.log(`Step ${i}: state=${result.state}, reward=${result.reward}, done=${result.done}`);
189
- console.log(env.render());
190
- if (result.done) break;
191
- }
192
-
193
- await env.reset();
194
- console.log('After reset:', env.getState());
195
- }
196
-
197
- testEnv();
198
- ```
@@ -1,251 +0,0 @@
1
- ---
2
- title: Custom Storage Backend
3
- description: Implement your own storage backend
4
- ---
5
-
6
- # Custom Storage Backend
7
-
8
- Create a custom storage backend by extending the abstract `Storage` class.
9
-
10
- ## Interface to Implement
11
-
12
- ```typescript
13
- import { Storage } from 'lsji';
14
-
15
- abstract class Storage {
16
- abstract initialize(): Promise<void>;
17
- abstract close(): Promise<void>;
18
- abstract getSetting(key: string): Promise<{key: string, value: string} | null>;
19
- abstract setSetting(key: string, value: string | number): Promise<void>;
20
- abstract getQTable(): Promise<Array<{state: string, action: number, q_value: number}>>;
21
- abstract updateQ(state: string, action: number, qValue: number): Promise<void>;
22
- abstract addBattle(record: BattleRecord): Promise<void>;
23
- abstract getTodayBattleCount(): Promise<number>;
24
- abstract getPerformanceStats(): Promise<Array<{mode: string, total: number, win_rate: number}>>;
25
- }
26
-
27
- interface BattleRecord {
28
- mode: 'train' | 'test';
29
- handA: number;
30
- handB: number;
31
- reward: number;
32
- createdAt: string;
33
- }
34
- ```
35
-
36
- ## Example: Redis Storage
37
-
38
- ```typescript
39
- import { Storage } from 'lsji';
40
- import Redis from 'ioredis';
41
-
42
- class RedisStorage extends Storage {
43
- constructor(redisUrl = 'redis://localhost:6379') {
44
- super();
45
- this.redis = new Redis(redisUrl);
46
- this.keyPrefix = 'lsji:';
47
- }
48
-
49
- async initialize() {
50
- // Set default settings
51
- await this.setSetting('is_active', 1);
52
- }
53
-
54
- async close() {
55
- await this.redis.quit();
56
- }
57
-
58
- async getSetting(key) {
59
- const value = await this.redis.get(this.keyPrefix + 'setting:' + key);
60
- return value ? { key, value } : null;
61
- }
62
-
63
- async setSetting(key, value) {
64
- await this.redis.set(this.keyPrefix + 'setting:' + key, String(value));
65
- }
66
-
67
- async getQTable() {
68
- const keys = await this.redis.keys(this.keyPrefix + 'q:*');
69
- const results = [];
70
-
71
- for (const key of keys) {
72
- const value = await this.redis.get(key);
73
- const parts = key.replace(this.keyPrefix + 'q:', '').split(':');
74
- results.push({
75
- state: parts[0],
76
- action: parseInt(parts[1], 10),
77
- q_value: parseFloat(value)
78
- });
79
- }
80
-
81
- return results.sort((a, b) => a.state.localeCompare(b.state) || a.action - b.action);
82
- }
83
-
84
- async updateQ(state, action, qValue) {
85
- await this.redis.set(
86
- this.keyPrefix + `q:${state}:${action}`,
87
- qValue.toString()
88
- );
89
- }
90
-
91
- async addBattle(record) {
92
- const battleKey = this.keyPrefix + `battle:${Date.now()}:${Math.random()}`;
93
- await this.redis.hset(battleKey, {
94
- mode: record.mode,
95
- hand_a: record.handA.toString(),
96
- hand_b: record.handB.toString(),
97
- reward: record.reward.toString(),
98
- created_at: record.createdAt
99
- });
100
-
101
- // Add to sorted set for date queries
102
- await this.redis.zadd(
103
- this.keyPrefix + 'battles:by_date',
104
- new Date(record.createdAt).getTime(),
105
- battleKey
106
- );
107
- }
108
-
109
- async getTodayBattleCount() {
110
- const today = new Date();
111
- today.setHours(0, 0, 0, 0);
112
- const tomorrow = new Date(today);
113
- tomorrow.setDate(tomorrow.getDate() + 1);
114
-
115
- return this.redis.zcount(
116
- this.keyPrefix + 'battles:by_date',
117
- today.getTime(),
118
- tomorrow.getTime()
119
- );
120
- }
121
-
122
- async getPerformanceStats() {
123
- const keys = await this.redis.zrange(this.keyPrefix + 'battles:by_date', 0, -1);
124
- const stats = new Map();
125
-
126
- for (const key of keys) {
127
- const battle = await this.redis.hgetall(key);
128
- const mode = battle.mode;
129
-
130
- if (!stats.has(mode)) {
131
- stats.set(mode, { total: 0, wins: 0 });
132
- }
133
-
134
- const stat = stats.get(mode);
135
- stat.total++;
136
- if (parseInt(battle.reward) > 0) stat.wins++;
137
- }
138
-
139
- return Array.from(stats.entries()).map(([mode, stat]) => ({
140
- mode,
141
- total: stat.total,
142
- win_rate: stat.total > 0 ? Math.round((stat.wins / stat.total) * 1000) / 10 : 0
143
- }));
144
- }
145
- }
146
- ```
147
-
148
- ## Register Custom Storage
149
-
150
- Add to `createStorage` factory (or use directly):
151
-
152
- ```typescript
153
- import { createStorage } from 'lsji';
154
-
155
- // Option 1: Use directly
156
- const storage = new RedisStorage('redis://localhost:6379');
157
- await storage.initialize();
158
-
159
- // Option 2: Extend createStorage (modify src/storage/index.js)
160
- import { RedisStorage } from './redis-storage';
161
-
162
- // In your code:
163
- const storage = new RedisStorage();
164
- await storage.initialize();
165
- ```
166
-
167
- ## Example: PostgreSQL Storage
168
-
169
- ```typescript
170
- import { Pool } from 'pg';
171
-
172
- class PostgresStorage extends Storage {
173
- constructor(connectionString) {
174
- super();
175
- this.pool = new Pool({ connectionString });
176
- }
177
-
178
- async initialize() {
179
- await this.pool.query(`
180
- CREATE TABLE IF NOT EXISTS settings (
181
- key TEXT PRIMARY KEY,
182
- value TEXT NOT NULL
183
- );
184
- CREATE TABLE IF NOT EXISTS battle_history (
185
- id SERIAL PRIMARY KEY,
186
- mode TEXT NOT NULL,
187
- hand_a INTEGER NOT NULL,
188
- hand_b INTEGER NOT NULL,
189
- reward INTEGER NOT NULL,
190
- created_at TIMESTAMP NOT NULL
191
- );
192
- CREATE TABLE IF NOT EXISTS q_table (
193
- state TEXT NOT NULL,
194
- action INTEGER NOT NULL,
195
- q_value REAL NOT NULL DEFAULT 0,
196
- PRIMARY KEY (state, action)
197
- );
198
- `);
199
- await this.setSetting('is_active', 1);
200
- }
201
-
202
- async close() {
203
- await this.pool.end();
204
- }
205
-
206
- async getSetting(key) {
207
- const res = await this.pool.query('SELECT key, value FROM settings WHERE key = $1', [key]);
208
- return res.rows[0] || null;
209
- }
210
-
211
- async setSetting(key, value) {
212
- await this.pool.query(
213
- `INSERT INTO settings (key, value) VALUES ($1, $2)
214
- ON CONFLICT (key) DO UPDATE SET value = $2`,
215
- [key, String(value)]
216
- );
217
- }
218
-
219
- // ... implement other methods similarly
220
- }
221
- ```
222
-
223
- ## Testing Custom Storage
224
-
225
- ```typescript
226
- import { MemoryStorage } from 'lsji';
227
-
228
- // Use MemoryStorage as reference implementation for testing
229
- async function testStorageImplementation(StorageClass) {
230
- const storage = new StorageClass();
231
- await storage.initialize();
232
-
233
- // Test settings
234
- await storage.setSetting('test', 'value');
235
- const setting = await storage.getSetting('test');
236
- assert(setting.value === 'value');
237
-
238
- // Test Q-table
239
- await storage.updateQ('state1', 0, 0.5);
240
- const qTable = await storage.getQTable();
241
- assert(qTable[0].q_value === 0.5);
242
-
243
- // Test battles
244
- await storage.addBattle({ mode: 'train', handA: 0, handB: 1, reward: 1, createdAt: new Date().toISOString() });
245
- const count = await storage.getTodayBattleCount();
246
- assert(count === 1);
247
-
248
- await storage.close();
249
- console.log('All tests passed!');
250
- }
251
- ```