@salesforce/magen-mcp-workflow 0.0.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/README.md +168 -0
- package/dist/checkpointing/index.d.ts +3 -0
- package/dist/checkpointing/index.js +10 -0
- package/dist/checkpointing/index.js.map +1 -0
- package/dist/checkpointing/jsonCheckpointer.d.ts +19 -0
- package/dist/checkpointing/jsonCheckpointer.js +164 -0
- package/dist/checkpointing/jsonCheckpointer.js.map +1 -0
- package/dist/checkpointing/statePersistence.d.ts +26 -0
- package/dist/checkpointing/statePersistence.js +117 -0
- package/dist/checkpointing/statePersistence.js.map +1 -0
- package/dist/checkpointing/workflowStateManager.d.ts +99 -0
- package/dist/checkpointing/workflowStateManager.js +206 -0
- package/dist/checkpointing/workflowStateManager.js.map +1 -0
- package/dist/common/fileSystem.d.ts +113 -0
- package/dist/common/fileSystem.js +62 -0
- package/dist/common/fileSystem.js.map +1 -0
- package/dist/common/metadata.d.ts +93 -0
- package/dist/common/metadata.js +40 -0
- package/dist/common/metadata.js.map +1 -0
- package/dist/common/propertyMetadata.d.ts +58 -0
- package/dist/common/propertyMetadata.js +8 -0
- package/dist/common/propertyMetadata.js.map +1 -0
- package/dist/common/types.d.ts +16 -0
- package/dist/common/types.js +8 -0
- package/dist/common/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.d.ts +56 -0
- package/dist/logging/logger.js +113 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/nodes/abstractBaseNode.d.ts +27 -0
- package/dist/nodes/abstractBaseNode.js +34 -0
- package/dist/nodes/abstractBaseNode.js.map +1 -0
- package/dist/nodes/abstractToolNode.d.ts +32 -0
- package/dist/nodes/abstractToolNode.js +44 -0
- package/dist/nodes/abstractToolNode.js.map +1 -0
- package/dist/nodes/getUserInput/factory.d.ts +42 -0
- package/dist/nodes/getUserInput/factory.js +64 -0
- package/dist/nodes/getUserInput/factory.js.map +1 -0
- package/dist/nodes/getUserInput/index.d.ts +2 -0
- package/dist/nodes/getUserInput/index.js +3 -0
- package/dist/nodes/getUserInput/index.js.map +1 -0
- package/dist/nodes/getUserInput/node.d.ts +68 -0
- package/dist/nodes/getUserInput/node.js +41 -0
- package/dist/nodes/getUserInput/node.js.map +1 -0
- package/dist/nodes/index.d.ts +5 -0
- package/dist/nodes/index.js +12 -0
- package/dist/nodes/index.js.map +1 -0
- package/dist/nodes/toolExecutor.d.ts +22 -0
- package/dist/nodes/toolExecutor.js +19 -0
- package/dist/nodes/toolExecutor.js.map +1 -0
- package/dist/nodes/userInputExtraction/factory.d.ts +42 -0
- package/dist/nodes/userInputExtraction/factory.js +55 -0
- package/dist/nodes/userInputExtraction/factory.js.map +1 -0
- package/dist/nodes/userInputExtraction/index.d.ts +2 -0
- package/dist/nodes/userInputExtraction/index.js +3 -0
- package/dist/nodes/userInputExtraction/index.js.map +1 -0
- package/dist/nodes/userInputExtraction/node.d.ts +60 -0
- package/dist/nodes/userInputExtraction/node.js +24 -0
- package/dist/nodes/userInputExtraction/node.js.map +1 -0
- package/dist/routers/checkPropertiesFulfilledRouter.d.ts +74 -0
- package/dist/routers/checkPropertiesFulfilledRouter.js +106 -0
- package/dist/routers/checkPropertiesFulfilledRouter.js.map +1 -0
- package/dist/routers/index.d.ts +1 -0
- package/dist/routers/index.js +8 -0
- package/dist/routers/index.js.map +1 -0
- package/dist/services/abstractService.d.ts +71 -0
- package/dist/services/abstractService.js +83 -0
- package/dist/services/abstractService.js.map +1 -0
- package/dist/services/getInputService.d.ts +43 -0
- package/dist/services/getInputService.js +48 -0
- package/dist/services/getInputService.js.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/inputExtractionService.d.ts +46 -0
- package/dist/services/inputExtractionService.js +133 -0
- package/dist/services/inputExtractionService.js.map +1 -0
- package/dist/storage/wellKnownDirectory.d.ts +79 -0
- package/dist/storage/wellKnownDirectory.js +121 -0
- package/dist/storage/wellKnownDirectory.js.map +1 -0
- package/dist/tools/base/abstractTool.d.ts +61 -0
- package/dist/tools/base/abstractTool.js +87 -0
- package/dist/tools/base/abstractTool.js.map +1 -0
- package/dist/tools/base/abstractWorkflowTool.d.ts +34 -0
- package/dist/tools/base/abstractWorkflowTool.js +94 -0
- package/dist/tools/base/abstractWorkflowTool.js.map +1 -0
- package/dist/tools/base/index.d.ts +2 -0
- package/dist/tools/base/index.js +9 -0
- package/dist/tools/base/index.js.map +1 -0
- package/dist/tools/orchestrator/config.d.ts +55 -0
- package/dist/tools/orchestrator/config.js +8 -0
- package/dist/tools/orchestrator/config.js.map +1 -0
- package/dist/tools/orchestrator/index.d.ts +3 -0
- package/dist/tools/orchestrator/index.js +9 -0
- package/dist/tools/orchestrator/index.js.map +1 -0
- package/dist/tools/orchestrator/metadata.d.ts +55 -0
- package/dist/tools/orchestrator/metadata.js +49 -0
- package/dist/tools/orchestrator/metadata.js.map +1 -0
- package/dist/tools/orchestrator/orchestratorTool.d.ts +39 -0
- package/dist/tools/orchestrator/orchestratorTool.js +186 -0
- package/dist/tools/orchestrator/orchestratorTool.js.map +1 -0
- package/dist/tools/utilities/getInput/factory.d.ts +43 -0
- package/dist/tools/utilities/getInput/factory.js +32 -0
- package/dist/tools/utilities/getInput/factory.js.map +1 -0
- package/dist/tools/utilities/getInput/index.d.ts +3 -0
- package/dist/tools/utilities/getInput/index.js +10 -0
- package/dist/tools/utilities/getInput/index.js.map +1 -0
- package/dist/tools/utilities/getInput/metadata.d.ts +78 -0
- package/dist/tools/utilities/getInput/metadata.js +43 -0
- package/dist/tools/utilities/getInput/metadata.js.map +1 -0
- package/dist/tools/utilities/getInput/tool.d.ts +89 -0
- package/dist/tools/utilities/getInput/tool.js +69 -0
- package/dist/tools/utilities/getInput/tool.js.map +1 -0
- package/dist/tools/utilities/index.d.ts +2 -0
- package/dist/tools/utilities/index.js +9 -0
- package/dist/tools/utilities/index.js.map +1 -0
- package/dist/tools/utilities/inputExtraction/factory.d.ts +43 -0
- package/dist/tools/utilities/inputExtraction/factory.js +32 -0
- package/dist/tools/utilities/inputExtraction/factory.js.map +1 -0
- package/dist/tools/utilities/inputExtraction/index.d.ts +3 -0
- package/dist/tools/utilities/inputExtraction/index.js +10 -0
- package/dist/tools/utilities/inputExtraction/index.js.map +1 -0
- package/dist/tools/utilities/inputExtraction/metadata.d.ts +66 -0
- package/dist/tools/utilities/inputExtraction/metadata.js +52 -0
- package/dist/tools/utilities/inputExtraction/metadata.js.map +1 -0
- package/dist/tools/utilities/inputExtraction/tool.d.ts +82 -0
- package/dist/tools/utilities/inputExtraction/tool.js +71 -0
- package/dist/tools/utilities/inputExtraction/tool.js.map +1 -0
- package/dist/utils/toolExecutionUtils.d.ts +55 -0
- package/dist/utils/toolExecutionUtils.js +70 -0
- package/dist/utils/toolExecutionUtils.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# @salesforce/magen-mcp-workflow
|
|
2
|
+
|
|
3
|
+
A reusable workflow orchestration framework for building deterministic, multi-step workflows with LangGraph and the Model Context Protocol (MCP).
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`mcp-workflow` is a **framework for MCP server authors** who need to orchestrate complex, multi-step processes involving LLM interactions. It provides the infrastructure for workflow execution, state management, and MCP tool coordination, while allowing consumers to define their own domain-specific logic.
|
|
8
|
+
|
|
9
|
+
### What This Framework Provides
|
|
10
|
+
|
|
11
|
+
✅ **Infrastructure**:
|
|
12
|
+
|
|
13
|
+
- `OrchestratorTool`: Manages workflow execution and coordinates MCP tool invocations
|
|
14
|
+
- Base classes for MCP tools (`AbstractTool`, `AbstractWorkflowTool`)
|
|
15
|
+
- Base classes for workflow nodes (`BaseNode`, `AbstractToolNode`)
|
|
16
|
+
- State persistence and checkpointing (via `.magen/` directory)
|
|
17
|
+
- Logging infrastructure
|
|
18
|
+
- Dependency injection patterns for testability
|
|
19
|
+
|
|
20
|
+
### What Consumers Provide
|
|
21
|
+
|
|
22
|
+
❌ **Domain Logic**:
|
|
23
|
+
|
|
24
|
+
- Your own `StateGraph` definition (workflow structure)
|
|
25
|
+
- Your own state annotations (`Annotation.Root`)
|
|
26
|
+
- Your own workflow nodes (business operations)
|
|
27
|
+
- Your own MCP server tools (domain capabilities)
|
|
28
|
+
- Your own MCP server instance
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @salesforce/magen-mcp-workflow
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
|
|
40
|
+
import { OrchestratorTool, OrchestratorConfig, BaseNode } from '@salesforce/magen-mcp-workflow';
|
|
41
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
42
|
+
|
|
43
|
+
// 1. Define your state
|
|
44
|
+
const MyWorkflowState = Annotation.Root({
|
|
45
|
+
userMessage: Annotation<string>,
|
|
46
|
+
response: Annotation<string>,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
type State = typeof MyWorkflowState.State;
|
|
50
|
+
|
|
51
|
+
// 2. Create workflow nodes
|
|
52
|
+
class ProcessMessageNode extends BaseNode<State> {
|
|
53
|
+
constructor() {
|
|
54
|
+
super('ProcessMessage');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
execute = (state: State): Partial<State> => {
|
|
58
|
+
return {
|
|
59
|
+
response: `Processed: ${state.userMessage}`,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Build the workflow (uncompiled StateGraph)
|
|
65
|
+
const processNode = new ProcessMessageNode();
|
|
66
|
+
const workflow = new StateGraph(MyWorkflowState)
|
|
67
|
+
.addNode(processNode.name, processNode.execute)
|
|
68
|
+
.addEdge(START, processNode.name)
|
|
69
|
+
.addEdge(processNode.name, END);
|
|
70
|
+
|
|
71
|
+
// 4. Configure the orchestrator
|
|
72
|
+
const orchestratorConfig: OrchestratorConfig<typeof MyWorkflowState> = {
|
|
73
|
+
toolId: 'my-orchestrator',
|
|
74
|
+
title: 'My Orchestrator',
|
|
75
|
+
description: 'Orchestrates my workflow',
|
|
76
|
+
workflow, // Pass uncompiled StateGraph
|
|
77
|
+
// context defaults to { environment: 'production' }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 5. Create and register the orchestrator
|
|
81
|
+
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
82
|
+
const orchestrator = new OrchestratorTool(server, orchestratorConfig);
|
|
83
|
+
orchestrator.register({ readOnlyHint: false });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Core Concepts
|
|
87
|
+
|
|
88
|
+
### The `.magen` Directory
|
|
89
|
+
|
|
90
|
+
By convention, workflow state and logs are stored in a well-known directory (`.magen/`):
|
|
91
|
+
|
|
92
|
+
- **Workflow state**: `~/.magen/workflow-state.json`
|
|
93
|
+
- **Workflow logs**: `~/.magen/workflow_logs.json`
|
|
94
|
+
|
|
95
|
+
This can be overridden via the `PROJECT_PATH` environment variable:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
export PROJECT_PATH=/path/to/project
|
|
99
|
+
# State stored in: /path/to/project/.magen/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Environment Contexts
|
|
103
|
+
|
|
104
|
+
The orchestrator supports two execution environments:
|
|
105
|
+
|
|
106
|
+
- **`production`** (default): Uses `JsonCheckpointSaver` with `.magen/` directory persistence
|
|
107
|
+
- **`test`**: Uses `MemorySaver` for in-memory checkpointing (no file I/O)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const config: OrchestratorConfig<typeof MyWorkflowState> = {
|
|
111
|
+
// ...
|
|
112
|
+
context: { environment: 'test' }, // For testing
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Workflow State Management
|
|
117
|
+
|
|
118
|
+
Workflows maintain session continuity across stateless MCP tool invocations using lightweight state data (`thread_id`). The orchestrator handles:
|
|
119
|
+
|
|
120
|
+
- Starting new workflow sessions
|
|
121
|
+
- Resuming in-progress workflows
|
|
122
|
+
- Persisting state between invocations
|
|
123
|
+
- Managing LangGraph checkpoints
|
|
124
|
+
|
|
125
|
+
## Architecture
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
129
|
+
│ Your MCP Server │
|
|
130
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
131
|
+
│ │ Domain Logic │ │
|
|
132
|
+
│ │ - StateGraph (workflow structure) │ │
|
|
133
|
+
│ │ - Nodes (business operations) │ │
|
|
134
|
+
│ │ - Tools (domain capabilities) │ │
|
|
135
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
136
|
+
│ │ │
|
|
137
|
+
│ │ depends on │
|
|
138
|
+
│ ▼ │
|
|
139
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
140
|
+
│ │ @salesforce/magen-mcp-workflow │ │
|
|
141
|
+
│ │ - OrchestratorTool │ │
|
|
142
|
+
│ │ - Base Classes │ │
|
|
143
|
+
│ │ - Checkpointing │ │
|
|
144
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
145
|
+
└─────────────────────────────────────────────────────────────┘
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Design Philosophy
|
|
149
|
+
|
|
150
|
+
This is a **framework, not a library**. Every API is designed with consumers in mind:
|
|
151
|
+
|
|
152
|
+
1. **Configurability**: Consumers configure the workflow engine with their workflows
|
|
153
|
+
2. **Genericity**: All components are generic over state types
|
|
154
|
+
3. **Encapsulation**: Implementation details are hidden
|
|
155
|
+
4. **Convention over Configuration**: Sensible defaults with escape hatches
|
|
156
|
+
5. **Testability**: Dependency injection patterns throughout
|
|
157
|
+
|
|
158
|
+
## Documentation
|
|
159
|
+
|
|
160
|
+
For comprehensive documentation, examples, and API references, see the [documentation directory](../../docs/README.md).
|
|
161
|
+
|
|
162
|
+
## Contributing
|
|
163
|
+
|
|
164
|
+
This package is part of the `mobile-mcp-tools` monorepo. See the [main README](../../README.md) for contribution guidelines.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
export { JsonCheckpointSaver } from './jsonCheckpointer.js';
|
|
8
|
+
export { WorkflowStatePersistence } from './statePersistence.js';
|
|
9
|
+
export { WorkflowStateManager, } from './workflowStateManager.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checkpointing/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EACL,oBAAoB,GAGrB,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RunnableConfig } from '@langchain/core/runnables';
|
|
2
|
+
import { BaseCheckpointSaver, Checkpoint, CheckpointMetadata, CheckpointTuple } from '@langchain/langgraph';
|
|
3
|
+
import { CheckpointListOptions, PendingWrite } from '@langchain/langgraph-checkpoint';
|
|
4
|
+
export declare class JsonCheckpointSaver extends BaseCheckpointSaver {
|
|
5
|
+
private state;
|
|
6
|
+
getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;
|
|
7
|
+
put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata): Promise<RunnableConfig>;
|
|
8
|
+
putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
|
|
9
|
+
list(config: RunnableConfig, options?: CheckpointListOptions): AsyncGenerator<CheckpointTuple>;
|
|
10
|
+
deleteThread(threadId: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Exports the entire state of the checkpointer as a single JSON string.
|
|
13
|
+
*/
|
|
14
|
+
exportState(): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Imports and overwrites the entire state of the checkpointer.
|
|
17
|
+
*/
|
|
18
|
+
importState(jsonState: string): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
import { BaseCheckpointSaver, } from '@langchain/langgraph';
|
|
8
|
+
import { WRITES_IDX_MAP, } from '@langchain/langgraph-checkpoint';
|
|
9
|
+
function createCheckpointKey(config) {
|
|
10
|
+
const threadId = config.configurable?.thread_id;
|
|
11
|
+
const checkpointId = config.configurable?.checkpoint_id;
|
|
12
|
+
if (!threadId || !checkpointId) {
|
|
13
|
+
throw new Error(`Invalid config, missing thread_id or checkpoint_id: ${JSON.stringify(config.configurable)}`);
|
|
14
|
+
}
|
|
15
|
+
return `${threadId}:${checkpointId}`;
|
|
16
|
+
}
|
|
17
|
+
export class JsonCheckpointSaver extends BaseCheckpointSaver {
|
|
18
|
+
state = { version: 1, storage: {}, writes: {} };
|
|
19
|
+
async getTuple(config) {
|
|
20
|
+
const threadId = config.configurable?.thread_id;
|
|
21
|
+
if (!threadId) {
|
|
22
|
+
throw new Error('thread_id not found in config');
|
|
23
|
+
}
|
|
24
|
+
const checkpoints = this.state.storage[threadId];
|
|
25
|
+
if (!checkpoints || checkpoints.length === 0) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
// This logic correctly gets the latest checkpoint.
|
|
29
|
+
// In this implementation, the latest is always at index 0.
|
|
30
|
+
const latest = checkpoints[0];
|
|
31
|
+
const checkpointPromise = this.serde.loadsTyped('json', Buffer.from(latest.checkpoint, 'base64'));
|
|
32
|
+
const metadataPromise = this.serde.loadsTyped('json', Buffer.from(latest.metadata, 'base64'));
|
|
33
|
+
const [checkpoint, metadata] = await Promise.all([checkpointPromise, metadataPromise]);
|
|
34
|
+
// Rehydrate pending writes associated with this checkpoint
|
|
35
|
+
const pendingWrites = [];
|
|
36
|
+
const checkpointKey = createCheckpointKey({
|
|
37
|
+
configurable: { thread_id: threadId, checkpoint_id: checkpoint.id },
|
|
38
|
+
});
|
|
39
|
+
const savedWrites = this.state.writes[checkpointKey];
|
|
40
|
+
if (savedWrites) {
|
|
41
|
+
const parsedWrites = JSON.parse(savedWrites);
|
|
42
|
+
for (const [taskId, channel, valueBase64] of Object.values(parsedWrites)) {
|
|
43
|
+
pendingWrites.push([
|
|
44
|
+
taskId,
|
|
45
|
+
channel,
|
|
46
|
+
await this.serde.loadsTyped('json', Buffer.from(valueBase64, 'base64')),
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const checkpointTuple = {
|
|
51
|
+
config,
|
|
52
|
+
checkpoint,
|
|
53
|
+
metadata,
|
|
54
|
+
pendingWrites,
|
|
55
|
+
};
|
|
56
|
+
if (latest.parentId) {
|
|
57
|
+
checkpointTuple.parentConfig = {
|
|
58
|
+
configurable: { thread_id: threadId, checkpoint_id: latest.parentId },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return checkpointTuple;
|
|
62
|
+
}
|
|
63
|
+
async put(config, checkpoint, metadata) {
|
|
64
|
+
const threadId = config.configurable?.thread_id;
|
|
65
|
+
if (!threadId) {
|
|
66
|
+
throw new Error('thread_id not found in config');
|
|
67
|
+
}
|
|
68
|
+
if (!this.state.storage[threadId]) {
|
|
69
|
+
this.state.storage[threadId] = [];
|
|
70
|
+
}
|
|
71
|
+
const checkpointPromise = this.serde.dumpsTyped(checkpoint);
|
|
72
|
+
const metadataPromise = this.serde.dumpsTyped(metadata);
|
|
73
|
+
const [[, checkpointBytes], [, metadataBytes]] = await Promise.all([
|
|
74
|
+
checkpointPromise,
|
|
75
|
+
metadataPromise,
|
|
76
|
+
]);
|
|
77
|
+
const parentId = config.configurable?.checkpoint_id;
|
|
78
|
+
// unshift() adds to the beginning, ensuring the latest is always at index 0.
|
|
79
|
+
this.state.storage[threadId].unshift({
|
|
80
|
+
checkpoint: Buffer.from(checkpointBytes).toString('base64'),
|
|
81
|
+
metadata: Buffer.from(metadataBytes).toString('base64'),
|
|
82
|
+
parentId: parentId,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
configurable: { thread_id: threadId, checkpoint_id: checkpoint.id },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async putWrites(config, writes, taskId) {
|
|
89
|
+
const key = createCheckpointKey(config);
|
|
90
|
+
const existingWritesString = this.state.writes[key];
|
|
91
|
+
const existingWrites = existingWritesString
|
|
92
|
+
? JSON.parse(existingWritesString)
|
|
93
|
+
: {};
|
|
94
|
+
for (const [index, [channel, value]] of writes.entries()) {
|
|
95
|
+
const [, serializedValue] = await this.serde.dumpsTyped(value);
|
|
96
|
+
const valueBase64 = Buffer.from(serializedValue).toString('base64');
|
|
97
|
+
const innerKey = `${taskId}:${WRITES_IDX_MAP[channel] ?? index}`;
|
|
98
|
+
existingWrites[innerKey] = [taskId, channel, valueBase64];
|
|
99
|
+
}
|
|
100
|
+
this.state.writes[key] = JSON.stringify(existingWrites);
|
|
101
|
+
}
|
|
102
|
+
async *list(config, options) {
|
|
103
|
+
const threadId = config.configurable?.thread_id;
|
|
104
|
+
if (!threadId) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const checkpoints = this.state.storage[threadId] ?? [];
|
|
108
|
+
let count = 0;
|
|
109
|
+
for (const saved of checkpoints) {
|
|
110
|
+
const metadataPromise = this.serde.loadsTyped('json', Buffer.from(saved.metadata, 'base64'));
|
|
111
|
+
const metadata = (await metadataPromise);
|
|
112
|
+
// Apply filter if provided
|
|
113
|
+
if (options?.filter &&
|
|
114
|
+
!Object.entries(options.filter).every(([key, value]) => metadata[key] === value)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Apply limit if provided
|
|
118
|
+
if (options?.limit && count >= options.limit) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
count++;
|
|
122
|
+
const checkpoint = (await this.serde.loadsTyped('json', Buffer.from(saved.checkpoint, 'base64')));
|
|
123
|
+
const tuple = {
|
|
124
|
+
config: {
|
|
125
|
+
configurable: { thread_id: threadId, checkpoint_id: checkpoint.id },
|
|
126
|
+
},
|
|
127
|
+
checkpoint,
|
|
128
|
+
metadata,
|
|
129
|
+
};
|
|
130
|
+
if (saved.parentId) {
|
|
131
|
+
tuple.parentConfig = {
|
|
132
|
+
configurable: { thread_id: threadId, checkpoint_id: saved.parentId },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
yield tuple;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async deleteThread(threadId) {
|
|
139
|
+
delete this.state.storage[threadId];
|
|
140
|
+
for (const key of Object.keys(this.state.writes)) {
|
|
141
|
+
if (key.startsWith(`${threadId}:`)) {
|
|
142
|
+
delete this.state.writes[key];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Exports the entire state of the checkpointer as a single JSON string.
|
|
148
|
+
*/
|
|
149
|
+
async exportState() {
|
|
150
|
+
return JSON.stringify(this.state);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Imports and overwrites the entire state of the checkpointer.
|
|
154
|
+
*/
|
|
155
|
+
async importState(jsonState) {
|
|
156
|
+
const parsedState = JSON.parse(jsonState);
|
|
157
|
+
// Basic check for future migration logic
|
|
158
|
+
if (!parsedState.version) {
|
|
159
|
+
parsedState.version = 1;
|
|
160
|
+
}
|
|
161
|
+
this.state = parsedState;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=jsonCheckpointer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonCheckpointer.js","sourceRoot":"","sources":["../../src/checkpointing/jsonCheckpointer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility class for persisting and recalling LangGraph checkpointer state
|
|
3
|
+
*/
|
|
4
|
+
export declare class WorkflowStatePersistence {
|
|
5
|
+
private readonly storePath;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
constructor(storePath: string);
|
|
8
|
+
/**
|
|
9
|
+
* Reads the serialized state from disk if it exists
|
|
10
|
+
* @returns The serialized state string, or undefined if the file doesn't exist or is invalid
|
|
11
|
+
*/
|
|
12
|
+
readState(): Promise<string | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Writes the serialized state to disk
|
|
15
|
+
* @param serializedState The state to persist
|
|
16
|
+
*/
|
|
17
|
+
writeState(serializedState: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Deletes the persisted state file
|
|
20
|
+
*/
|
|
21
|
+
clearState(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a persisted state file exists
|
|
24
|
+
*/
|
|
25
|
+
stateExists(): Promise<boolean>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { createComponentLogger } from '../logging/logger.js';
|
|
10
|
+
/**
|
|
11
|
+
* Utility class for persisting and recalling LangGraph checkpointer state
|
|
12
|
+
*/
|
|
13
|
+
export class WorkflowStatePersistence {
|
|
14
|
+
storePath;
|
|
15
|
+
logger;
|
|
16
|
+
constructor(storePath) {
|
|
17
|
+
this.storePath = storePath;
|
|
18
|
+
this.logger = createComponentLogger('WorkflowStatePersistence');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reads the serialized state from disk if it exists
|
|
22
|
+
* @returns The serialized state string, or undefined if the file doesn't exist or is invalid
|
|
23
|
+
*/
|
|
24
|
+
async readState() {
|
|
25
|
+
try {
|
|
26
|
+
// Check if the file exists
|
|
27
|
+
await fs.access(this.storePath);
|
|
28
|
+
this.logger.info(`Reading checkpointer state from: ${this.storePath}`);
|
|
29
|
+
// Read the file content
|
|
30
|
+
const content = await fs.readFile(this.storePath, 'utf-8');
|
|
31
|
+
// Validate that it's valid JSON
|
|
32
|
+
JSON.parse(content);
|
|
33
|
+
this.logger.debug('Successfully read and validated checkpointer state', {
|
|
34
|
+
stateSize: content.length,
|
|
35
|
+
path: this.storePath,
|
|
36
|
+
});
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT') {
|
|
41
|
+
this.logger.info(`Checkpointer state file not found: ${this.storePath}. Starting with fresh state.`);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
else if (error instanceof SyntaxError) {
|
|
45
|
+
this.logger.warn(`Invalid JSON in state file: ${this.storePath}. Starting with fresh state.`, error);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.logger.error(`Failed to read checkpointer state from: ${this.storePath}`, error);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Writes the serialized state to disk
|
|
56
|
+
* @param serializedState The state to persist
|
|
57
|
+
*/
|
|
58
|
+
async writeState(serializedState) {
|
|
59
|
+
try {
|
|
60
|
+
// Ensure the directory exists
|
|
61
|
+
const storeDir = path.dirname(this.storePath);
|
|
62
|
+
await fs.mkdir(storeDir, { recursive: true });
|
|
63
|
+
// Validate that the state is valid JSON before writing
|
|
64
|
+
JSON.parse(serializedState);
|
|
65
|
+
this.logger.info(`Saving checkpointer state to: ${this.storePath}`);
|
|
66
|
+
// Write the state to a temporary file first, then rename for atomicity
|
|
67
|
+
const tempPath = `${this.storePath}.tmp`;
|
|
68
|
+
await fs.writeFile(tempPath, serializedState, 'utf-8');
|
|
69
|
+
await fs.rename(tempPath, this.storePath);
|
|
70
|
+
this.logger.debug('Successfully saved checkpointer state', {
|
|
71
|
+
stateSize: serializedState.length,
|
|
72
|
+
path: this.storePath,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof SyntaxError) {
|
|
77
|
+
this.logger.error(`Invalid JSON state provided for persistence. State not saved.`, error);
|
|
78
|
+
throw new Error(`Invalid serialized state: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.logger.error(`Failed to save checkpointer state to: ${this.storePath}`, error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Deletes the persisted state file
|
|
88
|
+
*/
|
|
89
|
+
async clearState() {
|
|
90
|
+
try {
|
|
91
|
+
await fs.unlink(this.storePath);
|
|
92
|
+
this.logger.info(`Cleared checkpointer state file: ${this.storePath}`);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error.code === 'ENOENT') {
|
|
96
|
+
this.logger.debug(`State file already doesn't exist: ${this.storePath}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.logger.error(`Failed to clear state file: ${this.storePath}`, error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Checks if a persisted state file exists
|
|
106
|
+
*/
|
|
107
|
+
async stateExists() {
|
|
108
|
+
try {
|
|
109
|
+
const stat = await fs.stat(this.storePath);
|
|
110
|
+
return stat.isFile();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=statePersistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statePersistence.js","sourceRoot":"","sources":["../../src/checkpointing/statePersistence.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,qBAAqB,EAAU,MAAM,sBAAsB,CAAC;AAErE;;GAEG;AACH,MAAM,OAAO,wBAAwB;IAEN;IADZ,MAAM,CAAS;IAChC,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;QAC5C,IAAI,CAAC,MAAM,GAAG,qBAAqB,CAAC,0BAA0B,CAAC,CAAC;IAClE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS;QACb,IAAI,CAAC;YACH,2BAA2B;YAC3B,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAEhC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oCAAoC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAEvE,wBAAwB;YACxB,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAE3D,gCAAgC;YAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAEpB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,oDAAoD,EAAE;gBACtE,SAAS,EAAE,OAAO,CAAC,MAAM;gBACzB,IAAI,EAAE,IAAI,CAAC,SAAS;aACrB,CAAC,CAAC;YAEH,OAAO,OAAO,CAAC;QACjB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,sCAAsC,IAAI,CAAC,SAAS,8BAA8B,CACnF,CAAC;gBACF,OAAO,SAAS,CAAC;YACnB,CAAC;iBAAM,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;gBACxC,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,+BAA+B,IAAI,CAAC,SAAS,8BAA8B,EAC3E,KAAK,CACN,CAAC;gBACF,OAAO,SAAS,CAAC;YACnB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,2CAA2C,IAAI,CAAC,SAAS,EAAE,EAC3D,KAAc,CACf,CAAC;gBACF,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,eAAuB;QACtC,IAAI,CAAC;YACH,8BAA8B;YAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC9C,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAE9C,uDAAuD;YACvD,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAE5B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAEpE,uEAAuE;YACvE,MAAM,QAAQ,GAAG,GAAG,IAAI,CAAC,SAAS,MAAM,CAAC;YACzC,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;YACvD,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAE1C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,EAAE;gBACzD,SAAS,EAAE,eAAe,CAAC,MAAM;gBACjC,IAAI,EAAE,IAAI,CAAC,SAAS;aACrB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;gBACjC,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,+DAA+D,EAC/D,KAAc,CACf,CAAC;gBACF,MAAM,IAAI,KAAK,CAAC,6BAA6B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAChE,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,yCAAyC,IAAI,CAAC,SAAS,EAAE,EACzD,KAAc,CACf,CAAC;gBACF,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oCAAoC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACzE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAC3E,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,IAAI,CAAC,SAAS,EAAE,EAAE,KAAc,CAAC,CAAC;gBACnF,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW;QACf,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3C,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
2
|
+
import { Logger } from '../logging/logger.js';
|
|
3
|
+
import { FileSystemOperations } from '../common/fileSystem.js';
|
|
4
|
+
/**
|
|
5
|
+
* Workflow execution environment
|
|
6
|
+
*/
|
|
7
|
+
export type WorkflowEnvironment = 'production' | 'test';
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for WorkflowStateManager
|
|
10
|
+
*/
|
|
11
|
+
export interface WorkflowStateManagerConfig {
|
|
12
|
+
/**
|
|
13
|
+
* Execution environment - determines checkpointing strategy
|
|
14
|
+
* - 'production': Uses JsonCheckpointSaver with .magen/ directory persistence
|
|
15
|
+
* - 'test': Uses MemorySaver for isolated, in-memory state (no file I/O)
|
|
16
|
+
* Default: 'production'
|
|
17
|
+
*/
|
|
18
|
+
environment?: WorkflowEnvironment;
|
|
19
|
+
/**
|
|
20
|
+
* Optional project path for well-known directory (.magen/)
|
|
21
|
+
* Used to customize the location of workflow state files
|
|
22
|
+
* Defaults to os.homedir() if not specified
|
|
23
|
+
*/
|
|
24
|
+
projectPath?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Optional filesystem operations implementation for dependency injection
|
|
27
|
+
* Defaults to NodeFileSystemOperations for production use
|
|
28
|
+
*/
|
|
29
|
+
fileSystemOperations?: FileSystemOperations;
|
|
30
|
+
/**
|
|
31
|
+
* Optional logger for state management operations
|
|
32
|
+
*/
|
|
33
|
+
logger?: Logger;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Manages workflow state persistence and checkpointer lifecycle
|
|
37
|
+
*
|
|
38
|
+
* This service encapsulates all workflow state management responsibilities:
|
|
39
|
+
* - Creating and configuring checkpointers (MemorySaver vs JsonCheckpointSaver)
|
|
40
|
+
* - Loading existing state from disk
|
|
41
|
+
* - Saving state to disk
|
|
42
|
+
* - Managing well-known directory paths
|
|
43
|
+
*
|
|
44
|
+
* This separation allows the orchestrator to focus on workflow execution logic
|
|
45
|
+
* while delegating all state management concerns to this service.
|
|
46
|
+
*/
|
|
47
|
+
export declare class WorkflowStateManager {
|
|
48
|
+
private readonly logger;
|
|
49
|
+
private readonly environment;
|
|
50
|
+
private readonly wellKnownDirectoryManager;
|
|
51
|
+
private readonly fileSystemOperations;
|
|
52
|
+
constructor(config?: WorkflowStateManagerConfig);
|
|
53
|
+
/**
|
|
54
|
+
* Creates a checkpointer configured for the current environment
|
|
55
|
+
*
|
|
56
|
+
* For 'test' environment:
|
|
57
|
+
* - Returns MemorySaver (in-memory, no file I/O)
|
|
58
|
+
*
|
|
59
|
+
* For 'production' environment:
|
|
60
|
+
* - Returns JsonCheckpointSaver
|
|
61
|
+
* - Automatically loads existing state from disk if available
|
|
62
|
+
* - Creates fresh checkpointer if no state exists
|
|
63
|
+
*
|
|
64
|
+
* @returns A configured checkpointer ready for use
|
|
65
|
+
*/
|
|
66
|
+
createCheckpointer(): Promise<BaseCheckpointSaver>;
|
|
67
|
+
/**
|
|
68
|
+
* Saves checkpointer state to disk (production mode only)
|
|
69
|
+
*
|
|
70
|
+
* Only applies to JsonCheckpointSaver. MemorySaver (used in test mode)
|
|
71
|
+
* intentionally does not persist state.
|
|
72
|
+
*
|
|
73
|
+
* @param checkpointer - The checkpointer to save
|
|
74
|
+
* @throws Error if test environment unexpectedly has JsonCheckpointSaver
|
|
75
|
+
*/
|
|
76
|
+
saveCheckpointerState(checkpointer: BaseCheckpointSaver): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Reads the serialized state from disk if it exists
|
|
79
|
+
* @returns The serialized state string, or undefined if the file doesn't exist or is invalid
|
|
80
|
+
*/
|
|
81
|
+
private readState;
|
|
82
|
+
/**
|
|
83
|
+
* Writes the serialized state to disk
|
|
84
|
+
* @param serializedState The state to persist
|
|
85
|
+
*/
|
|
86
|
+
private writeState;
|
|
87
|
+
/**
|
|
88
|
+
* Deletes the persisted state file
|
|
89
|
+
*/
|
|
90
|
+
clearState(): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Checks if a persisted state file exists
|
|
93
|
+
*/
|
|
94
|
+
stateExists(): Promise<boolean>;
|
|
95
|
+
/**
|
|
96
|
+
* Gets the file path for workflow state storage
|
|
97
|
+
*/
|
|
98
|
+
private getStatePath;
|
|
99
|
+
}
|