@mi8y/cds-langgraph-persistence 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/.changeset/README.md +8 -0
- package/.changeset/bright-windows-leave.md +5 -0
- package/.changeset/config.json +11 -0
- package/.github/dependabot.yml +72 -0
- package/.github/workflows/ci.yaml +159 -0
- package/.github/workflows/pr-lint.yaml +58 -0
- package/.github/workflows/release.yml +50 -0
- package/AGENTS.md +34 -0
- package/CLAUDE.md +1 -0
- package/CONTRIBUTING.md +33 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/cds-plugin.js +0 -0
- package/eslint.config.mjs +20 -0
- package/index.cds +23 -0
- package/package.json +69 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/checkpoint/cds-checkpointer.ts +393 -0
- package/src/index.ts +1 -0
- package/tests/checkpoint/cds-checkpointer.test.ts +47 -0
- package/tsconfig.json +13 -0
- package/vite.config.ts +50 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changesets
|
|
2
|
+
|
|
3
|
+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
4
|
+
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
5
|
+
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
|
|
6
|
+
|
|
7
|
+
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
|
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
|
4
|
+
"commit": false,
|
|
5
|
+
"fixed": [],
|
|
6
|
+
"linked": [],
|
|
7
|
+
"access": "public",
|
|
8
|
+
"baseBranch": "main",
|
|
9
|
+
"updateInternalDependencies": "patch",
|
|
10
|
+
"ignore": []
|
|
11
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Please see the documentation for all configuration options:
|
|
2
|
+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
3
|
+
# and
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm"
|
|
9
|
+
directory: "/"
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "monthly"
|
|
12
|
+
|
|
13
|
+
groups:
|
|
14
|
+
# Internal LangChain packages
|
|
15
|
+
langchain:
|
|
16
|
+
patterns:
|
|
17
|
+
- "@langchain/*"
|
|
18
|
+
- "langchain"
|
|
19
|
+
|
|
20
|
+
# Vite, Vitest, and build tools
|
|
21
|
+
vite-vitest-turbo:
|
|
22
|
+
patterns:
|
|
23
|
+
- "vite"
|
|
24
|
+
- "vite-*"
|
|
25
|
+
- "vitest"
|
|
26
|
+
- "vitest-*"
|
|
27
|
+
- "@vitest/*"
|
|
28
|
+
- "rollup"
|
|
29
|
+
- "esbuild"
|
|
30
|
+
- "esbuild-*"
|
|
31
|
+
- "turbo"
|
|
32
|
+
|
|
33
|
+
# TypeScript, ESLint, and formatting
|
|
34
|
+
typescript-eslint-prettier:
|
|
35
|
+
patterns:
|
|
36
|
+
- "typescript"
|
|
37
|
+
- "@typescript-eslint/*"
|
|
38
|
+
- "@tsconfig/*"
|
|
39
|
+
- "eslint"
|
|
40
|
+
- "eslint-*"
|
|
41
|
+
- "@eslint/*"
|
|
42
|
+
- "prettier"
|
|
43
|
+
|
|
44
|
+
# SAP
|
|
45
|
+
sap:
|
|
46
|
+
patterns:
|
|
47
|
+
- "@sap/*"
|
|
48
|
+
- "@cap-js/*"
|
|
49
|
+
|
|
50
|
+
# Maintenance
|
|
51
|
+
cli-tools:
|
|
52
|
+
patterns:
|
|
53
|
+
- "@changesets/*"
|
|
54
|
+
|
|
55
|
+
# Type definitions
|
|
56
|
+
types:
|
|
57
|
+
patterns:
|
|
58
|
+
- "@types/*"
|
|
59
|
+
|
|
60
|
+
# Everything else — minor and patch together, major separately
|
|
61
|
+
minor-and-patch:
|
|
62
|
+
patterns:
|
|
63
|
+
- "*"
|
|
64
|
+
update-types:
|
|
65
|
+
- "minor"
|
|
66
|
+
- "patch"
|
|
67
|
+
|
|
68
|
+
major:
|
|
69
|
+
patterns:
|
|
70
|
+
- "*"
|
|
71
|
+
update-types:
|
|
72
|
+
- "major"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
3
|
+
|
|
4
|
+
name: ci
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: ["main"]
|
|
9
|
+
pull_request:
|
|
10
|
+
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
# If another push to the same PR or branch happens while this workflow is still running,
|
|
16
|
+
# cancel the earlier run in favor of the next run.
|
|
17
|
+
concurrency:
|
|
18
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
19
|
+
cancel-in-progress: true
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
# ===========================================
|
|
23
|
+
# Static checks + Build (all run in parallel)
|
|
24
|
+
# ===========================================
|
|
25
|
+
lint:
|
|
26
|
+
name: check linting
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
strategy:
|
|
29
|
+
matrix:
|
|
30
|
+
node-version: [24]
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v7
|
|
33
|
+
|
|
34
|
+
- name: Install pnpm
|
|
35
|
+
uses: pnpm/action-setup@v6
|
|
36
|
+
|
|
37
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
38
|
+
uses: actions/setup-node@v6
|
|
39
|
+
with:
|
|
40
|
+
node-version: ${{ matrix.node-version }}
|
|
41
|
+
cache: "pnpm"
|
|
42
|
+
|
|
43
|
+
- name: Install dependencies
|
|
44
|
+
run: pnpm install --frozen-lockfile
|
|
45
|
+
|
|
46
|
+
- name: Check linting
|
|
47
|
+
run: pnpm lint:check
|
|
48
|
+
|
|
49
|
+
format:
|
|
50
|
+
name: check formatting
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
strategy:
|
|
53
|
+
matrix:
|
|
54
|
+
node-version: [24]
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v7
|
|
57
|
+
|
|
58
|
+
- name: Install pnpm
|
|
59
|
+
uses: pnpm/action-setup@v6
|
|
60
|
+
|
|
61
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
62
|
+
uses: actions/setup-node@v6
|
|
63
|
+
with:
|
|
64
|
+
node-version: ${{ matrix.node-version }}
|
|
65
|
+
cache: "pnpm"
|
|
66
|
+
|
|
67
|
+
- name: Install dependencies
|
|
68
|
+
run: pnpm install --frozen-lockfile
|
|
69
|
+
|
|
70
|
+
- name: Check formatting
|
|
71
|
+
run: pnpm format:check
|
|
72
|
+
|
|
73
|
+
build:
|
|
74
|
+
name: build
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
strategy:
|
|
77
|
+
matrix:
|
|
78
|
+
node-version: [24]
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@v7
|
|
81
|
+
|
|
82
|
+
- name: Install pnpm
|
|
83
|
+
uses: pnpm/action-setup@v6
|
|
84
|
+
|
|
85
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
86
|
+
uses: actions/setup-node@v6
|
|
87
|
+
with:
|
|
88
|
+
node-version: ${{ matrix.node-version }}
|
|
89
|
+
cache: "pnpm"
|
|
90
|
+
|
|
91
|
+
- name: Install dependencies
|
|
92
|
+
run: pnpm install --frozen-lockfile
|
|
93
|
+
|
|
94
|
+
- name: Build
|
|
95
|
+
run: pnpm build
|
|
96
|
+
|
|
97
|
+
- name: Create build archive
|
|
98
|
+
run: tar -czf build-artifacts.tar.gz dist
|
|
99
|
+
|
|
100
|
+
- name: Publish to pkg.pr.new
|
|
101
|
+
if: ${{ github.event_name == 'pull_request' }}
|
|
102
|
+
run: pnpm dlx pkg-pr-new publish './*.tgz'
|
|
103
|
+
continue-on-error: true
|
|
104
|
+
|
|
105
|
+
- name: Upload build artifacts
|
|
106
|
+
uses: actions/upload-artifact@v7
|
|
107
|
+
with:
|
|
108
|
+
name: build-artifacts
|
|
109
|
+
path: build-artifacts.tar.gz
|
|
110
|
+
retention-days: 1
|
|
111
|
+
|
|
112
|
+
# ============================================
|
|
113
|
+
# Stage 2: Tests (require build + all static checks to pass)
|
|
114
|
+
# ============================================
|
|
115
|
+
test:
|
|
116
|
+
name: unit tests
|
|
117
|
+
needs: [lint, format, build]
|
|
118
|
+
runs-on: ubuntu-latest
|
|
119
|
+
permissions:
|
|
120
|
+
contents: read
|
|
121
|
+
code-quality: write
|
|
122
|
+
pull-requests: read
|
|
123
|
+
strategy:
|
|
124
|
+
matrix:
|
|
125
|
+
node-version: [24]
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v7
|
|
128
|
+
|
|
129
|
+
- name: Install pnpm
|
|
130
|
+
uses: pnpm/action-setup@v6
|
|
131
|
+
|
|
132
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
133
|
+
uses: actions/setup-node@v6
|
|
134
|
+
with:
|
|
135
|
+
node-version: ${{ matrix.node-version }}
|
|
136
|
+
cache: "pnpm"
|
|
137
|
+
|
|
138
|
+
- name: Install dependencies
|
|
139
|
+
run: pnpm install --frozen-lockfile
|
|
140
|
+
|
|
141
|
+
- name: Download build artifacts
|
|
142
|
+
uses: actions/download-artifact@v8
|
|
143
|
+
with:
|
|
144
|
+
name: build-artifacts
|
|
145
|
+
|
|
146
|
+
- name: Extract build artifacts
|
|
147
|
+
run: tar -xzf build-artifacts.tar.gz
|
|
148
|
+
shell: bash
|
|
149
|
+
|
|
150
|
+
- name: Test
|
|
151
|
+
run: pnpm test
|
|
152
|
+
|
|
153
|
+
- name: Upload coverage report
|
|
154
|
+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
|
155
|
+
uses: actions/upload-code-coverage@v1
|
|
156
|
+
with:
|
|
157
|
+
file: coverage/cobertura-coverage.xml
|
|
158
|
+
language: JavaScript
|
|
159
|
+
label: coverage
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Enforces Conventional Commits format for pull request titles.
|
|
2
|
+
name: pr title lint
|
|
3
|
+
|
|
4
|
+
permissions:
|
|
5
|
+
pull-requests: read
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
pull_request:
|
|
9
|
+
types: [opened, edited, synchronize]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
lint-pr-title:
|
|
13
|
+
name: validate title format
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: "Reject empty scope"
|
|
17
|
+
env:
|
|
18
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
19
|
+
run: |
|
|
20
|
+
if [[ "$PR_TITLE" =~ ^[a-z]+\(\)[!]?: ]]; then
|
|
21
|
+
echo "::error::PR title has empty scope parentheses: '$PR_TITLE'"
|
|
22
|
+
echo "Either remove the parentheses or provide a scope (e.g., 'fix(langgraph): ...')."
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
- name: "Validate Conventional Commits format"
|
|
27
|
+
uses: amannn/action-semantic-pull-request@v6
|
|
28
|
+
env:
|
|
29
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
30
|
+
with:
|
|
31
|
+
types: |
|
|
32
|
+
feat
|
|
33
|
+
fix
|
|
34
|
+
docs
|
|
35
|
+
style
|
|
36
|
+
refactor
|
|
37
|
+
perf
|
|
38
|
+
test
|
|
39
|
+
build
|
|
40
|
+
ci
|
|
41
|
+
chore
|
|
42
|
+
revert
|
|
43
|
+
release
|
|
44
|
+
hotfix
|
|
45
|
+
scopes: |
|
|
46
|
+
cds-langgraph-persistence
|
|
47
|
+
docs
|
|
48
|
+
examples
|
|
49
|
+
internal
|
|
50
|
+
infra
|
|
51
|
+
deps
|
|
52
|
+
deps-dev
|
|
53
|
+
requireScope: false
|
|
54
|
+
disallowScopes: |
|
|
55
|
+
release
|
|
56
|
+
[A-Z]+
|
|
57
|
+
ignoreLabels: |
|
|
58
|
+
ignore-lint-pr-title
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
name: release to npm
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
id-token: write
|
|
20
|
+
strategy:
|
|
21
|
+
matrix:
|
|
22
|
+
node-version: [24]
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout Repo
|
|
25
|
+
uses: actions/checkout@v7
|
|
26
|
+
|
|
27
|
+
- name: Install pnpm
|
|
28
|
+
uses: pnpm/action-setup@v6
|
|
29
|
+
|
|
30
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
31
|
+
uses: actions/setup-node@v6
|
|
32
|
+
with:
|
|
33
|
+
node-version: ${{ matrix.node-version }}
|
|
34
|
+
registry-url: "https://registry.npmjs.org"
|
|
35
|
+
package-manager-cache: false
|
|
36
|
+
|
|
37
|
+
- name: Install Dependencies
|
|
38
|
+
run: pnpm install --frozen-lockfile
|
|
39
|
+
|
|
40
|
+
- name: Create Release Pull Request or Publish to npm
|
|
41
|
+
id: changesets
|
|
42
|
+
uses: changesets/action@v1
|
|
43
|
+
with:
|
|
44
|
+
version: pnpm run version-packages
|
|
45
|
+
publish: pnpm run publish-packages
|
|
46
|
+
commit: "chore: version packages"
|
|
47
|
+
title: "chore: version packages"
|
|
48
|
+
env:
|
|
49
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
50
|
+
NPM_TOKEN: '' # # Use OIDC for npm authentication
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# CDS Persistence Plugin for LangGraph Checkpoint & Memory
|
|
2
|
+
|
|
3
|
+
This project builds a CDS Plugin for SAP CAP applications to build LangGraph/LangChain/Deep Agents based applications with Checkpoint and Memory persistence. It provides necessary tooling as well as library support for agents built within SAP CAP applications.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
/
|
|
8
|
+
├── package.json
|
|
9
|
+
├── AGENTS.md <- You are here
|
|
10
|
+
├── src
|
|
11
|
+
│ ├── checkpoint
|
|
12
|
+
│ │ ├── cds-checkpointer.ts
|
|
13
|
+
│ │ └── index.ts
|
|
14
|
+
│ └── memory
|
|
15
|
+
│ ├── cds-memory.ts
|
|
16
|
+
│ └── index.ts
|
|
17
|
+
├── tests
|
|
18
|
+
│ ├── checkpoint
|
|
19
|
+
│ └── memory
|
|
20
|
+
└── cds-plugin.js
|
|
21
|
+
|
|
22
|
+
## Development
|
|
23
|
+
|
|
24
|
+
- Project uses **`pnpm`** as package manager. `npm install -g pnpm` if not already installed.
|
|
25
|
+
- `pnpm install` to install dependencies.
|
|
26
|
+
|
|
27
|
+
## Testing
|
|
28
|
+
|
|
29
|
+
- Uses Vitest framework and follows `*.test.ts` naming convention.
|
|
30
|
+
- `pnpm test` to run tests
|
|
31
|
+
|
|
32
|
+
## Contributing
|
|
33
|
+
|
|
34
|
+
Read [CONTRIBUTING.md](CONTRIBUTING.md)
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Scripts
|
|
10
|
+
|
|
11
|
+
| Script | Description |
|
|
12
|
+
| ------------------- | -------------------------------- |
|
|
13
|
+
| `pnpm test` | Run the test suite |
|
|
14
|
+
| `pnpm lint:check` | Run all linters without writing |
|
|
15
|
+
| `pnpm lint` | Auto-fix lint issues |
|
|
16
|
+
| `pnpm format:check` | Check formatting without writing |
|
|
17
|
+
| `pnpm format` | Format source files |
|
|
18
|
+
| `pnpm changeset` | Wizard-based changeset creation |
|
|
19
|
+
|
|
20
|
+
## Style Guides
|
|
21
|
+
|
|
22
|
+
- While raising a pull-request, ensure that
|
|
23
|
+
- PR title follows the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) format.
|
|
24
|
+
- Build is passing - `pnpm build` before committing.
|
|
25
|
+
- All tests are passing - `pnpm test` before committing.
|
|
26
|
+
- Code is formatted - `pnpm format` and linted - `pnpm lint` before committing.
|
|
27
|
+
- Ensure the code is well documented
|
|
28
|
+
- Ensure the code is well tested and test coverage is not reduced.
|
|
29
|
+
- _Optionally_, a changeset `pnpm changeset` is created . If you are unsure, you can skip this step and the maintainers will create a changeset for you.
|
|
30
|
+
|
|
31
|
+
## Behaviour
|
|
32
|
+
|
|
33
|
+
- The changeset bot will listen for pull requests being opened and pull requests that have been updated, upon which it will then scan through the files for a changeset that has been added. If not, it will comment on the pull request to remind the contributor to add a changeset.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mi8Y Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# CDS Plugin for LangGraph Checkpoint & Memory
|
package/cds-plugin.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import js from "@eslint/js";
|
|
4
|
+
import { defineConfig } from "eslint/config";
|
|
5
|
+
import tseslint from "typescript-eslint";
|
|
6
|
+
import cds from "@sap/eslint-plugin-cds";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
files: ["src/**/*.{js,ts}", "**/*.cds"],
|
|
10
|
+
extends: [
|
|
11
|
+
js.configs.recommended,
|
|
12
|
+
tseslint.configs.recommended,
|
|
13
|
+
cds.configs.recommended,
|
|
14
|
+
{
|
|
15
|
+
rules: {
|
|
16
|
+
"@typescript-eslint/no-unused-expressions": "off", // cds uses expressions for 'column' in queries
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
});
|
package/index.cds
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
namespace plugin.langgraph.persistence;
|
|
2
|
+
|
|
3
|
+
entity Checkpoints {
|
|
4
|
+
key id : String(256) not null;
|
|
5
|
+
key namespace : String(256) not null default '';
|
|
6
|
+
key threadId : String(256) not null;
|
|
7
|
+
parentId : String(256);
|
|
8
|
+
type : String(64);
|
|
9
|
+
checkpoint : LargeString not null;
|
|
10
|
+
metadata : LargeString;
|
|
11
|
+
createdAt : Timestamp default $now;
|
|
12
|
+
writes : Composition of many CheckpointWrites
|
|
13
|
+
on writes.checkpoint = $self;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
entity CheckpointWrites {
|
|
17
|
+
key checkpoint : Association to Checkpoints;
|
|
18
|
+
key taskId : String(256) not null;
|
|
19
|
+
key idx : Integer not null;
|
|
20
|
+
channel : String(256) not null;
|
|
21
|
+
type : String(64);
|
|
22
|
+
value : LargeString;
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mi8y/cds-langgraph-persistence",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CDS Plugin for enabling persistence in LangGraph/LangChain/DeepAgents - Checkpoint & Memory",
|
|
5
|
+
"author": "Merbin J Anselm <merbin.j.anselm@mi8y.com>",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mi8y/cds-langgraph-persistence.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"homepage": "https://github.com/mi8y/cds-langgraph-persistence",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/mi8y/cds-langgraph-persistence/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.mjs",
|
|
22
|
+
"require": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@langchain/langgraph-checkpoint": "^1.1.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@cap-js/cds-typer": "^0.40.0",
|
|
30
|
+
"@cap-js/cds-types": "^0.18.0",
|
|
31
|
+
"@cap-js/sqlite": "^3.0.2",
|
|
32
|
+
"@changesets/cli": "^2.31.0",
|
|
33
|
+
"@eslint/js": "^10.0.1",
|
|
34
|
+
"@langchain/core": "^1.2.1",
|
|
35
|
+
"@langchain/langgraph-checkpoint-validation": "^1.1.0",
|
|
36
|
+
"@sap/eslint-plugin-cds": "^4.2.4",
|
|
37
|
+
"@tsconfig/recommended": "^1.0.13",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
39
|
+
"eslint": "^10.6.0",
|
|
40
|
+
"prettier": "^3.9.4",
|
|
41
|
+
"shx": "^0.4.0",
|
|
42
|
+
"typescript": "^6.0.3",
|
|
43
|
+
"typescript-eslint": "^8.62.1",
|
|
44
|
+
"vite": "^8.1.3",
|
|
45
|
+
"vite-plugin-dts": "^5.0.3",
|
|
46
|
+
"vite-tsconfig-paths": "^6.1.1",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@sap/cds": ">=8"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "npm run build:cds-types && vite build --watch",
|
|
54
|
+
"build": "shx rm -rf dist && npm run build:cds-types && vite build",
|
|
55
|
+
"build:cds-types": "cds-typer index.cds",
|
|
56
|
+
"test": "vitest run --coverage",
|
|
57
|
+
"lint": "eslint . --fix",
|
|
58
|
+
"lint:check": "eslint . --max-warnings 0",
|
|
59
|
+
"format": "prettier --write \"**/*.{json,ts,js,mjs,cjs,md}\"",
|
|
60
|
+
"format:check": "prettier --check \"**/*.{json,ts,js,mjs,cjs,md}\"",
|
|
61
|
+
"changeset": "changeset",
|
|
62
|
+
"version-packages": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format",
|
|
63
|
+
"publish-packages": "changeset publish",
|
|
64
|
+
"clean": "shx rm -rf node_modules dist"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Checkpoints,
|
|
3
|
+
CheckpointWrite,
|
|
4
|
+
CheckpointWrites,
|
|
5
|
+
} from "#cds-models/plugin/langgraph/persistence";
|
|
6
|
+
import { type RunnableConfig } from "@langchain/core/runnables";
|
|
7
|
+
import {
|
|
8
|
+
BaseCheckpointSaver,
|
|
9
|
+
ChannelVersions,
|
|
10
|
+
Checkpoint,
|
|
11
|
+
CheckpointListOptions,
|
|
12
|
+
CheckpointMetadata,
|
|
13
|
+
CheckpointTuple,
|
|
14
|
+
copyCheckpoint,
|
|
15
|
+
maxChannelVersion,
|
|
16
|
+
PendingWrite,
|
|
17
|
+
SerializerProtocol,
|
|
18
|
+
TASKS,
|
|
19
|
+
WRITES_IDX_MAP,
|
|
20
|
+
} from "@langchain/langgraph-checkpoint";
|
|
21
|
+
|
|
22
|
+
export class CdsCheckpointSaver extends BaseCheckpointSaver {
|
|
23
|
+
constructor(serde?: SerializerProtocol) {
|
|
24
|
+
super(serde);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {
|
|
28
|
+
if (!config.configurable) {
|
|
29
|
+
throw new Error(`Empty "config.configurable" supplied`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const checkpointId: string | undefined = config.configurable.checkpoint_id;
|
|
33
|
+
const checkpointNamespace: string = config.configurable.checkpoint_ns ?? "";
|
|
34
|
+
const threadId: string | undefined = config.configurable.thread_id;
|
|
35
|
+
if (!threadId) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let query = SELECT.one
|
|
40
|
+
.from(Checkpoints)
|
|
41
|
+
.columns((c) => {
|
|
42
|
+
(c.threadId,
|
|
43
|
+
c.namespace,
|
|
44
|
+
c.id,
|
|
45
|
+
c.parentId,
|
|
46
|
+
c.type,
|
|
47
|
+
c.checkpoint,
|
|
48
|
+
c.metadata,
|
|
49
|
+
c.writes((w) => {
|
|
50
|
+
(w.taskId, w.channel, w.type, w.value);
|
|
51
|
+
}));
|
|
52
|
+
})
|
|
53
|
+
.where({
|
|
54
|
+
threadId: threadId,
|
|
55
|
+
namespace: checkpointNamespace,
|
|
56
|
+
...(checkpointId ? { id: checkpointId } : {}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!checkpointId) {
|
|
60
|
+
query = query.orderBy("id desc").limit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resCheckpoint = await query;
|
|
64
|
+
if (!resCheckpoint) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const checkpoint = (await this.serde.loadsTyped(
|
|
69
|
+
resCheckpoint.type ?? "json",
|
|
70
|
+
resCheckpoint.checkpoint ?? "",
|
|
71
|
+
)) as Checkpoint;
|
|
72
|
+
|
|
73
|
+
const metadata = (await this.serde.loadsTyped(
|
|
74
|
+
resCheckpoint.type ?? "json",
|
|
75
|
+
resCheckpoint.metadata ?? "",
|
|
76
|
+
)) as CheckpointMetadata;
|
|
77
|
+
|
|
78
|
+
const pendingWrites = await Promise.all(
|
|
79
|
+
(resCheckpoint.writes ?? []).map(async (w) => {
|
|
80
|
+
return [
|
|
81
|
+
w.taskId,
|
|
82
|
+
w.channel,
|
|
83
|
+
await this.serde.loadsTyped(w.type ?? "json", w.value ?? ""),
|
|
84
|
+
] as [string, string, unknown];
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (checkpoint.v < 4 && resCheckpoint.parentId) {
|
|
89
|
+
await this.migratePendingSends(
|
|
90
|
+
checkpoint,
|
|
91
|
+
resCheckpoint.threadId!,
|
|
92
|
+
resCheckpoint.parentId,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
checkpoint: checkpoint,
|
|
98
|
+
config: {
|
|
99
|
+
configurable: {
|
|
100
|
+
thread_id: resCheckpoint.threadId,
|
|
101
|
+
checkpoint_ns: resCheckpoint.namespace,
|
|
102
|
+
checkpoint_id: resCheckpoint.id,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
parentConfig: resCheckpoint.parentId
|
|
106
|
+
? {
|
|
107
|
+
configurable: {
|
|
108
|
+
thread_id: resCheckpoint.threadId,
|
|
109
|
+
checkpoint_ns: resCheckpoint.namespace,
|
|
110
|
+
checkpoint_id: resCheckpoint.parentId,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
: undefined,
|
|
114
|
+
metadata: metadata,
|
|
115
|
+
pendingWrites: pendingWrites,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async *list(
|
|
120
|
+
config: RunnableConfig,
|
|
121
|
+
options?: CheckpointListOptions,
|
|
122
|
+
): AsyncGenerator<CheckpointTuple> {
|
|
123
|
+
const { limit, before, filter } = options ?? {};
|
|
124
|
+
|
|
125
|
+
const threadId: string | undefined = config.configurable?.thread_id;
|
|
126
|
+
const checkpointNamespace: string | undefined =
|
|
127
|
+
config.configurable?.checkpoint_ns;
|
|
128
|
+
|
|
129
|
+
let query = SELECT.from(Checkpoints)
|
|
130
|
+
.columns((c) => {
|
|
131
|
+
(c.threadId,
|
|
132
|
+
c.namespace,
|
|
133
|
+
c.id,
|
|
134
|
+
c.parentId,
|
|
135
|
+
c.type,
|
|
136
|
+
c.checkpoint,
|
|
137
|
+
c.metadata,
|
|
138
|
+
c.writes((w) => {
|
|
139
|
+
(w.taskId, w.channel, w.type, w.value);
|
|
140
|
+
}));
|
|
141
|
+
})
|
|
142
|
+
.orderBy("id desc");
|
|
143
|
+
|
|
144
|
+
if (threadId !== undefined) {
|
|
145
|
+
query = query.where({ threadId: threadId });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (checkpointNamespace !== undefined && checkpointNamespace !== null) {
|
|
149
|
+
query = query.where({ namespace: checkpointNamespace });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (before?.configurable?.checkpoint_id !== undefined) {
|
|
153
|
+
query = query.where({ id: { "<": before.configurable.checkpoint_id } });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// CAP CDS does not support native JSON operations - so we filter them in memory after fetching the results
|
|
157
|
+
const sanitizedFilter = Object.fromEntries(
|
|
158
|
+
Object.entries(filter ?? {}).filter(([, value]) => value !== undefined),
|
|
159
|
+
);
|
|
160
|
+
const hasFilter = Object.keys(sanitizedFilter).length > 0;
|
|
161
|
+
|
|
162
|
+
if (limit !== undefined && !hasFilter) {
|
|
163
|
+
query = query.limit(limit);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const resCheckpoints = await query;
|
|
167
|
+
if (!resCheckpoints) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let yielded = 0;
|
|
172
|
+
for (const checkpointState of resCheckpoints) {
|
|
173
|
+
if (limit !== undefined && yielded >= limit) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const deserializedMetadata = (await this.serde.loadsTyped(
|
|
178
|
+
checkpointState.type ?? "json",
|
|
179
|
+
checkpointState.metadata ?? "",
|
|
180
|
+
)) as CheckpointMetadata & Record<string, unknown>;
|
|
181
|
+
|
|
182
|
+
if (hasFilter) {
|
|
183
|
+
// since not filtered in the query, we filter in memory
|
|
184
|
+
const matchesFilter = Object.entries(sanitizedFilter).every(
|
|
185
|
+
([key, value]) => deserializedMetadata[key] === value,
|
|
186
|
+
);
|
|
187
|
+
if (!matchesFilter) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const checkpoint = (await this.serde.loadsTyped(
|
|
193
|
+
checkpointState.type ?? "json",
|
|
194
|
+
checkpointState.checkpoint ?? "",
|
|
195
|
+
)) as Checkpoint;
|
|
196
|
+
|
|
197
|
+
const pendingWrites = await Promise.all(
|
|
198
|
+
(checkpointState.writes ?? []).map(async (w) => {
|
|
199
|
+
return [
|
|
200
|
+
w.taskId,
|
|
201
|
+
w.channel,
|
|
202
|
+
await this.serde.loadsTyped(w.type ?? "json", w.value ?? ""),
|
|
203
|
+
] as [string, string, unknown];
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (checkpoint.v < 4 && checkpointState.parentId) {
|
|
208
|
+
await this.migratePendingSends(
|
|
209
|
+
checkpoint,
|
|
210
|
+
checkpointState.threadId!,
|
|
211
|
+
checkpointState.parentId,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
yield {
|
|
216
|
+
config: {
|
|
217
|
+
configurable: {
|
|
218
|
+
thread_id: checkpointState.threadId,
|
|
219
|
+
checkpoint_ns: checkpointState.namespace,
|
|
220
|
+
checkpoint_id: checkpointState.id,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
checkpoint: checkpoint,
|
|
224
|
+
parentConfig: checkpointState.parentId
|
|
225
|
+
? {
|
|
226
|
+
configurable: {
|
|
227
|
+
thread_id: checkpointState.threadId,
|
|
228
|
+
checkpoint_ns: checkpointState.namespace,
|
|
229
|
+
checkpoint_id: checkpointState.parentId,
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
: undefined,
|
|
233
|
+
metadata: deserializedMetadata,
|
|
234
|
+
pendingWrites: pendingWrites,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
yielded++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async put(
|
|
242
|
+
config: RunnableConfig,
|
|
243
|
+
checkpoint: Checkpoint,
|
|
244
|
+
metadata: CheckpointMetadata,
|
|
245
|
+
newVersions: ChannelVersions,
|
|
246
|
+
): Promise<RunnableConfig> {
|
|
247
|
+
if (!config.configurable) {
|
|
248
|
+
throw new Error(`Empty "config.configurable" supplied`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const checkpointNamespace: string = config.configurable.checkpoint_ns ?? "";
|
|
252
|
+
const parentCheckpointId: string | undefined =
|
|
253
|
+
config.configurable.checkpoint_id;
|
|
254
|
+
const threadId: string | undefined = config.configurable.thread_id;
|
|
255
|
+
if (!threadId) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Missing "thread_id" field in passed in "config.configurable"`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const newChannelValues = Object.fromEntries(
|
|
262
|
+
Object.entries(checkpoint.channel_values).filter(
|
|
263
|
+
([key]) => key in newVersions,
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
const newChannelVersions = Object.fromEntries(
|
|
267
|
+
Object.entries(checkpoint.channel_versions).filter(
|
|
268
|
+
([key]) => key in newVersions,
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const filteredCheckpoint = {
|
|
273
|
+
...checkpoint,
|
|
274
|
+
channel_values: newChannelValues,
|
|
275
|
+
channel_versions: newChannelVersions,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const preparedCheckpoint: Partial<Checkpoint> =
|
|
279
|
+
copyCheckpoint(filteredCheckpoint);
|
|
280
|
+
|
|
281
|
+
const [[type1, serializedCheckpoint], [type2, serializedMetadata]] =
|
|
282
|
+
await Promise.all([
|
|
283
|
+
this.serde.dumpsTyped(preparedCheckpoint),
|
|
284
|
+
this.serde.dumpsTyped(metadata),
|
|
285
|
+
]);
|
|
286
|
+
if (type1 !== type2) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
"Failed to serialized checkpoint and metadata to the same type.",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const valueDecoder = new TextDecoder("utf-8");
|
|
293
|
+
await UPSERT.into(Checkpoints).entries({
|
|
294
|
+
id: checkpoint.id,
|
|
295
|
+
namespace: checkpointNamespace,
|
|
296
|
+
threadId: threadId,
|
|
297
|
+
parentId: parentCheckpointId,
|
|
298
|
+
type: type1,
|
|
299
|
+
checkpoint: valueDecoder.decode(serializedCheckpoint),
|
|
300
|
+
metadata: valueDecoder.decode(serializedMetadata),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
configurable: {
|
|
305
|
+
thread_id: threadId,
|
|
306
|
+
checkpoint_ns: checkpointNamespace,
|
|
307
|
+
checkpoint_id: checkpoint.id,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async putWrites(
|
|
313
|
+
config: RunnableConfig,
|
|
314
|
+
writes: PendingWrite[],
|
|
315
|
+
taskId: string,
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
if (!config.configurable) {
|
|
318
|
+
throw new Error(`Empty "config.configurable" supplied`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const checkpointNamespace: string = config.configurable.checkpoint_ns ?? "";
|
|
322
|
+
const checkpointId: string | undefined = config.configurable.checkpoint_id;
|
|
323
|
+
if (!checkpointId) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Missing "checkpoint_id" field in passed in "config.configurable"`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const threadId: string | undefined = config.configurable.thread_id;
|
|
329
|
+
if (!threadId) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Missing "thread_id" field in passed in "config.configurable"`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const pendingWrites: CheckpointWrite[] = await Promise.all(
|
|
336
|
+
writes.map(async (write, idx) => {
|
|
337
|
+
const [type, serializedValue] = await this.serde.dumpsTyped(write[1]);
|
|
338
|
+
const valueDecoder = new TextDecoder("utf-8");
|
|
339
|
+
return {
|
|
340
|
+
threadId: threadId,
|
|
341
|
+
checkpoint_id: checkpointId,
|
|
342
|
+
checkpoint_namespace: checkpointNamespace,
|
|
343
|
+
checkpoint_threadId: threadId,
|
|
344
|
+
taskId: taskId,
|
|
345
|
+
// Special channels are stored at fixed negative indices so they
|
|
346
|
+
// never collide with regular per-step writes (whose `idx` is the
|
|
347
|
+
// ordinal within `writes`).
|
|
348
|
+
idx: WRITES_IDX_MAP[write[0]] ?? idx,
|
|
349
|
+
channel: write[0],
|
|
350
|
+
type: type,
|
|
351
|
+
value: valueDecoder.decode(serializedValue),
|
|
352
|
+
};
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
await UPSERT.into(CheckpointWrites).entries(pendingWrites);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async deleteThread(threadId: string): Promise<void> {
|
|
360
|
+
await DELETE.from(CheckpointWrites).where({
|
|
361
|
+
checkpoint_threadId: threadId,
|
|
362
|
+
});
|
|
363
|
+
await DELETE.from(Checkpoints).where({ threadId: threadId });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
protected async migratePendingSends(
|
|
367
|
+
checkpoint: Checkpoint,
|
|
368
|
+
threadId: string,
|
|
369
|
+
parentId: string,
|
|
370
|
+
) {
|
|
371
|
+
const parentWrites = await SELECT.from(CheckpointWrites)
|
|
372
|
+
.where({
|
|
373
|
+
checkpoint_threadId: threadId,
|
|
374
|
+
checkpoint_id: parentId,
|
|
375
|
+
channel: TASKS,
|
|
376
|
+
})
|
|
377
|
+
.orderBy("taskId", "idx");
|
|
378
|
+
|
|
379
|
+
if (parentWrites === undefined || parentWrites.length === 0) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
checkpoint.channel_values[TASKS] = await Promise.all(
|
|
384
|
+
parentWrites.map((w) =>
|
|
385
|
+
this.serde.loadsTyped(w.type ?? "json", w.value ?? ""),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
checkpoint.channel_versions[TASKS] =
|
|
389
|
+
Object.keys(checkpoint.channel_versions).length > 0
|
|
390
|
+
? maxChannelVersion(...Object.values(checkpoint.channel_versions))
|
|
391
|
+
: this.getNextVersion(undefined);
|
|
392
|
+
}
|
|
393
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CdsCheckpointSaver } from "@/checkpoint/cds-checkpointer";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Checkpoints,
|
|
3
|
+
CheckpointWrites,
|
|
4
|
+
} from "#cds-models/plugin/langgraph/persistence";
|
|
5
|
+
import { CdsCheckpointSaver } from "@/index";
|
|
6
|
+
import {
|
|
7
|
+
CheckpointSaverTestInitializer,
|
|
8
|
+
validate,
|
|
9
|
+
} from "@langchain/langgraph-checkpoint-validation";
|
|
10
|
+
import cds from "@sap/cds";
|
|
11
|
+
|
|
12
|
+
export const cdsCheckpointSaverTestInitializer: CheckpointSaverTestInitializer<CdsCheckpointSaver> =
|
|
13
|
+
{
|
|
14
|
+
checkpointerName: "@langchain/langgraph-checkpoint-cds",
|
|
15
|
+
|
|
16
|
+
async beforeAll() {
|
|
17
|
+
const csn = await cds.load("index.cds").then(cds.minify);
|
|
18
|
+
cds.model = cds.compile.for.nodejs(csn);
|
|
19
|
+
|
|
20
|
+
cds.requires.db = {
|
|
21
|
+
kind: "sqlite",
|
|
22
|
+
impl: "@cap-js/sqlite",
|
|
23
|
+
credentials: { url: ":memory:" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
cds.db = await cds.connect.to("db");
|
|
27
|
+
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
await cds.deploy("index.cds", {}).to(cds.db);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async afterAll() {
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
await cds.db.disconnect?.();
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async createCheckpointer() {
|
|
38
|
+
return new CdsCheckpointSaver();
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async destroyCheckpointer() {
|
|
42
|
+
await DELETE.from(CheckpointWrites);
|
|
43
|
+
await DELETE.from(Checkpoints);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
validate(cdsCheckpointSaverTestInitializer);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/recommended",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/*": ["./src/*"],
|
|
7
|
+
"@sap/cds": ["./node_modules/@cap-js/cds-types"],
|
|
8
|
+
"#cds-models/*": ["./@cds-models/*"]
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*", "tests/**/*"],
|
|
12
|
+
"exclude": ["node_modules", "dist", "@cds-models"]
|
|
13
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/// <reference types="vitest/config" />
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
import dts from "vite-plugin-dts";
|
|
4
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
5
|
+
import { builtinModules } from "module";
|
|
6
|
+
import pkg from "./package.json";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [
|
|
10
|
+
tsconfigPaths(),
|
|
11
|
+
dts({
|
|
12
|
+
insertTypesEntry: true, // Automatically links types in package.json
|
|
13
|
+
include: ["src"],
|
|
14
|
+
}),
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
// Build Configuration
|
|
18
|
+
build: {
|
|
19
|
+
minify: false, // disable since this is a node library
|
|
20
|
+
lib: {
|
|
21
|
+
entry: "src/index.ts",
|
|
22
|
+
name: "@mi8y/cds-langgraph-persistence",
|
|
23
|
+
fileName: "index",
|
|
24
|
+
formats: ["es", "cjs"],
|
|
25
|
+
},
|
|
26
|
+
sourcemap: true,
|
|
27
|
+
rolldownOptions: {
|
|
28
|
+
external: [
|
|
29
|
+
// ignore dependencies when bundling
|
|
30
|
+
...builtinModules,
|
|
31
|
+
...builtinModules.map((m) => `node:${m}`),
|
|
32
|
+
...Object.keys(pkg.dependencies || {}),
|
|
33
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Testing Configuration (Vitest)
|
|
39
|
+
test: {
|
|
40
|
+
globals: true,
|
|
41
|
+
environment: "node",
|
|
42
|
+
include: ["tests/**/*.ts"],
|
|
43
|
+
coverage: {
|
|
44
|
+
provider: "v8",
|
|
45
|
+
exclude: ["@cds-models/"],
|
|
46
|
+
reporter: ["cobertura"],
|
|
47
|
+
reportsDirectory: "coverage",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|