@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.
- package/package.json +15 -7
- package/src/cli.js +395 -62
- package/src/execution/budget/circuit-breaker.js +245 -0
- package/src/execution/budget/cost-tracker.js +387 -0
- package/src/execution/budget/index.js +63 -0
- package/src/execution/budget/token-counter.js +159 -0
- package/src/execution/engine.js +428 -0
- package/src/execution/hitl/approval-gate.js +210 -0
- package/src/execution/hitl/index.js +12 -0
- package/src/execution/hitl/notifier.js +151 -0
- package/src/execution/hitl/store.js +311 -0
- package/src/execution/idempotency.js +312 -0
- package/src/execution/index.js +14 -0
- package/src/index.js +80 -4
- package/src/llm/index.js +21 -0
- package/src/llm/llm-agent.js +357 -0
- package/src/llm/memory/conversation.js +271 -0
- package/src/llm/memory/episodic.js +312 -0
- package/src/llm/memory/index.js +12 -0
- package/src/llm/memory/semantic.js +324 -0
- package/src/llm/plugins/index.js +202 -0
- package/src/llm/prompt-manager.js +332 -0
- package/src/llm/providers/anthropic.js +250 -0
- package/src/llm/providers/base.js +116 -0
- package/src/llm/providers/local.js +163 -0
- package/src/llm/providers/openai.js +212 -0
- package/src/llm/tools/registry.js +342 -0
- package/src/server/index.js +416 -0
- package/src/server/ui/index.html +16 -0
- package/src/server/ui/package.json +19 -0
- package/src/server/ui/src/main.jsx +10 -0
- package/src/server/ui/src/styles.css +260 -0
- package/src/server/ui/vite.config.js +27 -0
- package/docs/README.md +0 -43
- package/docs/blog/2019-05-28-first-blog-post.mdx +0 -12
- package/docs/blog/2019-05-29-long-blog-post.mdx +0 -44
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.mdx +0 -29
- package/docs/blog/authors.yml +0 -25
- package/docs/blog/tags.yml +0 -19
- package/docs/docs/api/agent.md +0 -151
- package/docs/docs/api/env.md +0 -133
- package/docs/docs/api/environments.md +0 -102
- package/docs/docs/api/qlearning.md +0 -138
- package/docs/docs/api/storage.md +0 -168
- package/docs/docs/architecture.md +0 -155
- package/docs/docs/cli.md +0 -210
- package/docs/docs/contributing.md +0 -162
- package/docs/docs/core-concepts.md +0 -152
- package/docs/docs/examples/advanced-training.md +0 -244
- package/docs/docs/examples/custom-environment.md +0 -198
- package/docs/docs/examples/custom-storage.md +0 -251
- package/docs/docs/getting-started.md +0 -91
- package/docs/docusaurus.config.ts +0 -149
- package/docs/package-lock.json +0 -19522
- package/docs/package.json +0 -49
- package/docs/sidebars.ts +0 -33
- package/docs/src/components/HomepageFeatures/index.tsx +0 -71
- package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
- package/docs/src/css/custom.css +0 -79
- package/docs/src/pages/index.module.css +0 -23
- package/docs/src/pages/index.tsx +0 -44
- package/docs/src/pages/markdown-page.mdx +0 -7
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.png +0 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
- package/docs/static/img/undraw_docusaurus_react.svg +0 -170
- package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- package/docs/tsconfig.json +0 -12
- package/legacy/worker.js +0 -166
- package/legacy/wrangler.toml +0 -11
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Counter
|
|
3
|
+
*
|
|
4
|
+
* Tracks token usage across different LLM providers with accurate counting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createProvider } from '../../llm/providers/base.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Token usage record
|
|
11
|
+
* @typedef {Object} TokenUsage
|
|
12
|
+
* @property {number} inputTokens
|
|
13
|
+
* @property {number} outputTokens
|
|
14
|
+
* @property {number} totalTokens
|
|
15
|
+
* @property {string} model
|
|
16
|
+
* @property {string} provider
|
|
17
|
+
* @property {Date} timestamp
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Token Counter - Tracks and estimates token usage
|
|
22
|
+
*/
|
|
23
|
+
export class TokenCounter {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.usageHistory = [];
|
|
26
|
+
this.providerCache = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get or create provider instance for token estimation
|
|
31
|
+
*/
|
|
32
|
+
async getProvider(config) {
|
|
33
|
+
const key = `${config.provider}:${config.model}`;
|
|
34
|
+
if (!this.providerCache.has(key)) {
|
|
35
|
+
const provider = await createProvider(config);
|
|
36
|
+
this.providerCache.set(key, provider);
|
|
37
|
+
}
|
|
38
|
+
return this.providerCache.get(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Estimate tokens for messages using provider's tokenizer
|
|
43
|
+
*/
|
|
44
|
+
async estimateTokens(messages, providerConfig) {
|
|
45
|
+
const provider = await this.getProvider(providerConfig);
|
|
46
|
+
return provider.estimateTokens(messages);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Record actual token usage from a completion
|
|
51
|
+
*/
|
|
52
|
+
recordUsage(usage) {
|
|
53
|
+
const record = {
|
|
54
|
+
...usage,
|
|
55
|
+
timestamp: new Date(),
|
|
56
|
+
};
|
|
57
|
+
this.usageHistory.push(record);
|
|
58
|
+
return record;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get total tokens used in a time range
|
|
63
|
+
*/
|
|
64
|
+
getTotalTokens(since = null) {
|
|
65
|
+
let filtered = this.usageHistory;
|
|
66
|
+
if (since) {
|
|
67
|
+
filtered = this.usageHistory.filter(u => u.timestamp >= since);
|
|
68
|
+
}
|
|
69
|
+
return filtered.reduce((sum, u) => sum + (u.totalTokens || 0), 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get tokens by model
|
|
74
|
+
*/
|
|
75
|
+
getTokensByModel(since = null) {
|
|
76
|
+
let filtered = this.usageHistory;
|
|
77
|
+
if (since) {
|
|
78
|
+
filtered = this.usageHistory.filter(u => u.timestamp >= since);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const byModel = {};
|
|
82
|
+
for (const u of filtered) {
|
|
83
|
+
const model = u.model || 'unknown';
|
|
84
|
+
if (!byModel[model]) {
|
|
85
|
+
byModel[model] = { inputTokens: 0, outputTokens: 0, totalTokens: 0, count: 0 };
|
|
86
|
+
}
|
|
87
|
+
byModel[model].inputTokens += u.inputTokens || 0;
|
|
88
|
+
byModel[model].outputTokens += u.outputTokens || 0;
|
|
89
|
+
byModel[model].totalTokens += u.totalTokens || 0;
|
|
90
|
+
byModel[model].count += 1;
|
|
91
|
+
}
|
|
92
|
+
return byModel;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get tokens by provider
|
|
97
|
+
*/
|
|
98
|
+
getTokensByProvider(since = null) {
|
|
99
|
+
let filtered = this.usageHistory;
|
|
100
|
+
if (since) {
|
|
101
|
+
filtered = this.usageHistory.filter(u => u.timestamp >= since);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const byProvider = {};
|
|
105
|
+
for (const u of filtered) {
|
|
106
|
+
const provider = u.provider || 'unknown';
|
|
107
|
+
if (!byProvider[provider]) {
|
|
108
|
+
byProvider[provider] = { inputTokens: 0, outputTokens: 0, totalTokens: 0, count: 0 };
|
|
109
|
+
}
|
|
110
|
+
byProvider[provider].inputTokens += u.inputTokens || 0;
|
|
111
|
+
byProvider[provider].outputTokens += u.outputTokens || 0;
|
|
112
|
+
byProvider[provider].totalTokens += u.totalTokens || 0;
|
|
113
|
+
byProvider[provider].count += 1;
|
|
114
|
+
}
|
|
115
|
+
return byProvider;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get usage history
|
|
120
|
+
*/
|
|
121
|
+
getHistory(limit = 100) {
|
|
122
|
+
return this.usageHistory.slice(-limit);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear history
|
|
127
|
+
*/
|
|
128
|
+
clear() {
|
|
129
|
+
this.usageHistory = [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get summary statistics
|
|
134
|
+
*/
|
|
135
|
+
getSummary(since = null) {
|
|
136
|
+
let filtered = this.usageHistory;
|
|
137
|
+
if (since) {
|
|
138
|
+
filtered = this.usageHistory.filter(u => u.timestamp >= since);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const total = filtered.reduce((sum, u) => sum + (u.totalTokens || 0), 0);
|
|
142
|
+
const input = filtered.reduce((sum, u) => sum + (u.inputTokens || 0), 0);
|
|
143
|
+
const output = filtered.reduce((sum, u) => sum + (u.outputTokens || 0), 0);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
totalRequests: filtered.length,
|
|
147
|
+
totalTokens: total,
|
|
148
|
+
inputTokens: input,
|
|
149
|
+
outputTokens: output,
|
|
150
|
+
byModel: this.getTokensByModel(since),
|
|
151
|
+
byProvider: this.getTokensByProvider(since),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Global token counter instance
|
|
158
|
+
*/
|
|
159
|
+
export const globalTokenCounter = new TokenCounter();
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Engine
|
|
3
|
+
*
|
|
4
|
+
* Durable execution engine with automatic checkpointing and recovery.
|
|
5
|
+
* Provides workflow execution with resume capability.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createStorage } from '../index.js';
|
|
9
|
+
import { IdempotencyStore } from './idempotency.js';
|
|
10
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checkpoint record
|
|
14
|
+
* @typedef {Object} CheckpointRecord
|
|
15
|
+
* @property {string} id - Checkpoint ID
|
|
16
|
+
* @property {string} workflowId - Workflow identifier
|
|
17
|
+
* @property {number} step - Step number
|
|
18
|
+
* @property {Object} state - Workflow state at checkpoint
|
|
19
|
+
* @property {Object} context - Execution context
|
|
20
|
+
* @property {Date} createdAt
|
|
21
|
+
* @property {string} [parentId] - Parent checkpoint ID
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Workflow definition
|
|
26
|
+
* @typedef {Object} Workflow
|
|
27
|
+
* @property {string} id - Workflow ID
|
|
28
|
+
* @property {string} name - Workflow name
|
|
29
|
+
* @property {Function} execute - Async function(context) -> result
|
|
30
|
+
* @property {Array<string>} [checkpointSteps] - Steps to checkpoint after
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execution Engine - Durable workflow execution
|
|
35
|
+
*/
|
|
36
|
+
export class ExecutionEngine {
|
|
37
|
+
constructor({ storage, checkpointStore, idempotencyStore, defaultCheckpointInterval = 5 } = {}) {
|
|
38
|
+
this.storage = storage;
|
|
39
|
+
this.checkpointStore = checkpointStore;
|
|
40
|
+
this.idempotencyStore = idempotencyStore;
|
|
41
|
+
this.defaultCheckpointInterval = defaultCheckpointInterval;
|
|
42
|
+
this.initialized = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if storage is SQL-based (has db with all method)
|
|
47
|
+
*/
|
|
48
|
+
isSqlStorage() {
|
|
49
|
+
return this.storage.db && typeof this.storage.db.all === 'function';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Initialize checkpoint table
|
|
54
|
+
*/
|
|
55
|
+
async initialize() {
|
|
56
|
+
if (this.initialized) return;
|
|
57
|
+
|
|
58
|
+
if (this.isSqlStorage()) {
|
|
59
|
+
await this.storage.db.exec(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
workflow_id TEXT NOT NULL,
|
|
63
|
+
step INTEGER NOT NULL,
|
|
64
|
+
state TEXT NOT NULL,
|
|
65
|
+
context TEXT NOT NULL,
|
|
66
|
+
created_at TEXT NOT NULL,
|
|
67
|
+
parent_id TEXT
|
|
68
|
+
)
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
await this.storage.db.exec(`
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_workflow ON checkpoints(workflow_id)
|
|
73
|
+
`);
|
|
74
|
+
|
|
75
|
+
await this.storage.db.exec(`
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_created ON checkpoints(created_at)
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.initialized = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Execute a workflow with checkpointing
|
|
85
|
+
*/
|
|
86
|
+
async execute(workflow, options = {}) {
|
|
87
|
+
await this.initialize();
|
|
88
|
+
|
|
89
|
+
const workflowId = options.workflowId || workflow.id || uuidv4();
|
|
90
|
+
const checkpointEvery = options.checkpointEvery || this.defaultCheckpointInterval;
|
|
91
|
+
const idempotencyKey = options.idempotencyKey;
|
|
92
|
+
const resumeFrom = options.resumeFrom; // checkpoint ID to resume from
|
|
93
|
+
|
|
94
|
+
let context = options.initialContext || {};
|
|
95
|
+
let step = 0;
|
|
96
|
+
let parentCheckpointId = null;
|
|
97
|
+
|
|
98
|
+
// Resume from checkpoint if specified
|
|
99
|
+
if (resumeFrom) {
|
|
100
|
+
const checkpoint = await this.getCheckpoint(resumeFrom);
|
|
101
|
+
if (checkpoint) {
|
|
102
|
+
context = { ...context, ...checkpoint.state };
|
|
103
|
+
step = checkpoint.step;
|
|
104
|
+
parentCheckpointId = checkpoint.id;
|
|
105
|
+
console.log(`Resuming workflow ${workflowId} from checkpoint ${resumeFrom} (step ${step})`);
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error(`Checkpoint not found: ${resumeFrom}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wrap execution with idempotency if key provided
|
|
112
|
+
const executeWithIdempotency = async (fn) => {
|
|
113
|
+
if (idempotencyKey && this.idempotencyStore) {
|
|
114
|
+
return this.idempotencyStore.execute(
|
|
115
|
+
idempotencyKey,
|
|
116
|
+
workflowId,
|
|
117
|
+
{ workflowId, step, context },
|
|
118
|
+
fn
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return fn();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Execute workflow steps
|
|
126
|
+
const result = await executeWithIdempotency(async () => {
|
|
127
|
+
return await workflow.execute(context);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Save final checkpoint
|
|
131
|
+
await this.saveCheckpoint({
|
|
132
|
+
workflowId,
|
|
133
|
+
step: step + 1,
|
|
134
|
+
state: context,
|
|
135
|
+
context: { ...context, result },
|
|
136
|
+
parentId: parentCheckpointId,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return { success: true, result, workflowId, checkpoints: await this.getCheckpoints(workflowId) };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// Save error checkpoint
|
|
142
|
+
await this.saveCheckpoint({
|
|
143
|
+
workflowId,
|
|
144
|
+
step,
|
|
145
|
+
state: context,
|
|
146
|
+
context: { ...context, error: error.message },
|
|
147
|
+
parentId: parentCheckpointId,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return { success: false, error: error.message, workflowId, checkpoints: await this.getCheckpoints(workflowId) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Execute a multi-step workflow with per-step checkpointing
|
|
156
|
+
*/
|
|
157
|
+
async executeSteps(steps, options = {}) {
|
|
158
|
+
await this.initialize();
|
|
159
|
+
|
|
160
|
+
const workflowId = options.workflowId || uuidv4();
|
|
161
|
+
const checkpointEvery = options.checkpointEvery || this.defaultCheckpointInterval;
|
|
162
|
+
const idempotencyKey = options.idempotencyKey;
|
|
163
|
+
const resumeFrom = options.resumeFrom;
|
|
164
|
+
|
|
165
|
+
let context = options.initialContext || {};
|
|
166
|
+
let step = 0;
|
|
167
|
+
let parentCheckpointId = null;
|
|
168
|
+
|
|
169
|
+
// Resume from checkpoint
|
|
170
|
+
if (resumeFrom) {
|
|
171
|
+
const checkpoint = await this.getCheckpoint(resumeFrom);
|
|
172
|
+
if (checkpoint) {
|
|
173
|
+
context = { ...context, ...checkpoint.state };
|
|
174
|
+
step = checkpoint.step;
|
|
175
|
+
parentCheckpointId = checkpoint.id;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const results = [];
|
|
180
|
+
|
|
181
|
+
for (let i = step; i < steps.length; i++) {
|
|
182
|
+
const stepFn = steps[i];
|
|
183
|
+
step = i + 1;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const stepResult = await stepFn(context);
|
|
187
|
+
results.push({ step: i, success: true, result: stepResult });
|
|
188
|
+
context = { ...context, [steps[i].name || `step_${i}`]: stepResult };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
results.push({ step: i, success: false, error: error.message });
|
|
191
|
+
|
|
192
|
+
// Save error checkpoint
|
|
193
|
+
await this.saveCheckpoint({
|
|
194
|
+
workflowId,
|
|
195
|
+
step: i + 1,
|
|
196
|
+
state: context,
|
|
197
|
+
context: { ...context, error: error.message, step: i },
|
|
198
|
+
parentId: parentCheckpointId,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!options.continueOnError) {
|
|
202
|
+
return { success: false, results, workflowId, error: error.message };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Checkpoint at intervals
|
|
207
|
+
if (step % checkpointEvery === 0) {
|
|
208
|
+
await this.saveCheckpoint({
|
|
209
|
+
workflowId,
|
|
210
|
+
step,
|
|
211
|
+
state: context,
|
|
212
|
+
context: { ...context, lastStep: i },
|
|
213
|
+
parentId: parentCheckpointId,
|
|
214
|
+
});
|
|
215
|
+
parentCheckpointId = await this.getLatestCheckpointId(workflowId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Final checkpoint
|
|
220
|
+
await this.saveCheckpoint({
|
|
221
|
+
workflowId,
|
|
222
|
+
step: steps.length,
|
|
223
|
+
state: context,
|
|
224
|
+
context: { ...context, results },
|
|
225
|
+
parentId: parentCheckpointId,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return { success: true, results, workflowId, context };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Save a checkpoint
|
|
233
|
+
*/
|
|
234
|
+
async saveCheckpoint(data) {
|
|
235
|
+
await this.initialize();
|
|
236
|
+
|
|
237
|
+
const checkpoint = {
|
|
238
|
+
id: uuidv4(),
|
|
239
|
+
workflowId: data.workflowId,
|
|
240
|
+
step: data.step,
|
|
241
|
+
state: JSON.stringify(data.state),
|
|
242
|
+
context: JSON.stringify(data.context),
|
|
243
|
+
createdAt: new Date().toISOString(),
|
|
244
|
+
parentId: data.parentId || null,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (this.isSqlStorage()) {
|
|
248
|
+
await this.storage.db.run(
|
|
249
|
+
`INSERT INTO checkpoints (id, workflow_id, step, state, context, created_at, parent_id)
|
|
250
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
251
|
+
[checkpoint.id, checkpoint.workflowId, checkpoint.step, checkpoint.state,
|
|
252
|
+
checkpoint.context, checkpoint.createdAt, checkpoint.parentId]
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
if (!this.memoryCheckpoints) this.memoryCheckpoints = new Map();
|
|
256
|
+
this.memoryCheckpoints.set(checkpoint.id, checkpoint);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return checkpoint;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get a checkpoint by ID
|
|
264
|
+
*/
|
|
265
|
+
async getCheckpoint(id) {
|
|
266
|
+
await this.initialize();
|
|
267
|
+
|
|
268
|
+
if (this.isSqlStorage()) {
|
|
269
|
+
const row = await this.storage.db.get('SELECT * FROM checkpoints WHERE id = ?', [id]);
|
|
270
|
+
if (!row) return null;
|
|
271
|
+
return {
|
|
272
|
+
id: row.id,
|
|
273
|
+
workflowId: row.workflow_id,
|
|
274
|
+
step: row.step,
|
|
275
|
+
state: JSON.parse(row.state),
|
|
276
|
+
context: JSON.parse(row.context),
|
|
277
|
+
createdAt: row.created_at,
|
|
278
|
+
parentId: row.parent_id,
|
|
279
|
+
};
|
|
280
|
+
} else {
|
|
281
|
+
return this.memoryCheckpoints?.get(id) || null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get all checkpoints for a workflow
|
|
287
|
+
*/
|
|
288
|
+
async getCheckpoints(workflowId) {
|
|
289
|
+
await this.initialize();
|
|
290
|
+
|
|
291
|
+
if (this.isSqlStorage()) {
|
|
292
|
+
const rows = await this.storage.db.all(
|
|
293
|
+
'SELECT * FROM checkpoints WHERE workflow_id = ? ORDER BY step ASC',
|
|
294
|
+
[workflowId]
|
|
295
|
+
);
|
|
296
|
+
return rows.map(row => ({
|
|
297
|
+
id: row.id,
|
|
298
|
+
workflowId: row.workflow_id,
|
|
299
|
+
step: row.step,
|
|
300
|
+
state: JSON.parse(row.state),
|
|
301
|
+
context: JSON.parse(row.context),
|
|
302
|
+
createdAt: row.created_at,
|
|
303
|
+
parentId: row.parent_id,
|
|
304
|
+
}));
|
|
305
|
+
} else {
|
|
306
|
+
const checkpoints = [];
|
|
307
|
+
for (const cp of this.memoryCheckpoints?.values() || []) {
|
|
308
|
+
if (cp.workflowId === workflowId) {
|
|
309
|
+
checkpoints.push(cp);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return checkpoints.sort((a, b) => a.step - b.step);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get latest checkpoint ID for a workflow
|
|
318
|
+
*/
|
|
319
|
+
async getLatestCheckpointId(workflowId) {
|
|
320
|
+
const checkpoints = await this.getCheckpoints(workflowId);
|
|
321
|
+
return checkpoints.length > 0 ? checkpoints[checkpoints.length - 1].id : null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Recover workflow from latest checkpoint
|
|
326
|
+
*/
|
|
327
|
+
async recover(workflowId) {
|
|
328
|
+
const checkpoints = await this.getCheckpoints(workflowId);
|
|
329
|
+
if (checkpoints.length === 0) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const latest = checkpoints[checkpoints.length - 1];
|
|
334
|
+
return {
|
|
335
|
+
workflowId,
|
|
336
|
+
step: latest.step,
|
|
337
|
+
state: latest.state,
|
|
338
|
+
context: latest.context,
|
|
339
|
+
checkpointId: latest.id,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* List all workflows with checkpoints
|
|
345
|
+
*/
|
|
346
|
+
async listWorkflows() {
|
|
347
|
+
await this.initialize();
|
|
348
|
+
|
|
349
|
+
if (this.isSqlStorage()) {
|
|
350
|
+
const rows = await this.storage.db.all(`
|
|
351
|
+
SELECT workflow_id, MAX(step) as last_step, MAX(created_at) as last_checkpoint
|
|
352
|
+
FROM checkpoints
|
|
353
|
+
GROUP BY workflow_id
|
|
354
|
+
ORDER BY last_checkpoint DESC
|
|
355
|
+
`);
|
|
356
|
+
return rows;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// For memory storage, collect unique workflow IDs
|
|
360
|
+
const workflows = new Map();
|
|
361
|
+
for (const cp of this.memoryCheckpoints?.values() || []) {
|
|
362
|
+
if (!workflows.has(cp.workflowId)) {
|
|
363
|
+
workflows.set(cp.workflowId, {
|
|
364
|
+
workflow_id: cp.workflowId,
|
|
365
|
+
last_step: cp.step,
|
|
366
|
+
last_checkpoint: cp.createdAt,
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
const existing = workflows.get(cp.workflowId);
|
|
370
|
+
if (cp.step > existing.last_step) {
|
|
371
|
+
existing.last_step = cp.step;
|
|
372
|
+
existing.last_checkpoint = cp.createdAt;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return Array.from(workflows.values()).sort((a, b) =>
|
|
378
|
+
new Date(b.last_checkpoint) - new Date(a.last_checkpoint)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Delete checkpoints for a workflow
|
|
384
|
+
*/
|
|
385
|
+
async deleteCheckpoints(workflowId) {
|
|
386
|
+
await this.initialize();
|
|
387
|
+
|
|
388
|
+
if (this.isSqlStorage()) {
|
|
389
|
+
await this.storage.db.run('DELETE FROM checkpoints WHERE workflow_id = ?', [workflowId]);
|
|
390
|
+
} else {
|
|
391
|
+
for (const [key, cp] of this.memoryCheckpoints?.entries() || []) {
|
|
392
|
+
if (cp.workflowId === workflowId) {
|
|
393
|
+
this.memoryCheckpoints.delete(key);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Create execution engine from config
|
|
402
|
+
*/
|
|
403
|
+
export async function createExecutionEngine(config = {}) {
|
|
404
|
+
const storage = await createStorage(
|
|
405
|
+
config.storage?.type || 'sqlite',
|
|
406
|
+
config.storage?.options || {}
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
let checkpointStore = storage;
|
|
410
|
+
if (config.checkpointStorage) {
|
|
411
|
+
checkpointStore = await createStorage(
|
|
412
|
+
config.checkpointStorage.type || 'sqlite',
|
|
413
|
+
config.checkpointStorage.options || {}
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let idempotencyStore = null;
|
|
418
|
+
if (config.idempotency) {
|
|
419
|
+
idempotencyStore = await IdempotencyStore.create(config.idempotency);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return new ExecutionEngine({
|
|
423
|
+
storage,
|
|
424
|
+
checkpointStore,
|
|
425
|
+
idempotencyStore,
|
|
426
|
+
defaultCheckpointInterval: config.checkpointInterval,
|
|
427
|
+
});
|
|
428
|
+
}
|