@letsping/sdk 0.1.5 → 0.2.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/README.md +170 -12
- package/dist/index.d.ts +7 -0
- package/dist/index.js +71 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +71 -12
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/langgraph.d.mts +20 -0
- package/dist/integrations/langgraph.d.ts +17 -0
- package/dist/integrations/langgraph.js +150 -0
- package/dist/integrations/langgraph.js.map +1 -0
- package/dist/integrations/langgraph.mjs +125 -0
- package/dist/integrations/langgraph.mjs.map +1 -0
- package/examples/langgraph-demo.ts +80 -0
- package/package.json +20 -6
- package/src/index.ts +67 -7
- package/src/integrations/langgraph.ts +163 -0
- package/tsup.config.ts +1 -1
- package/tsc_error.log +0 -213
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/integrations/langgraph.ts"],"sourcesContent":["import { BaseCheckpointSaver, Checkpoint, CheckpointMetadata, CheckpointTuple } from \"@langchain/langgraph\";\r\nimport { RunnableConfig } from \"@langchain/core/runnables\";\r\nimport { LetsPing } from \"../index\";\r\n\r\ntype StoredCheckpoint = {\r\n checkpoint: Checkpoint;\r\n metadata: CheckpointMetadata;\r\n};\r\n\r\nexport class LetsPingCheckpointer extends BaseCheckpointSaver {\r\n private checkpoints: Record<string, StoredCheckpoint> = {};\r\n\r\n constructor(public client: LetsPing) {\r\n super();\r\n }\r\n\r\n private getTransport(): (<T = any>(method: string, path: string, body?: any) => Promise<T>) | null {\r\n const clientAny = this.client as any;\r\n if (typeof clientAny.request === \"function\") {\r\n return clientAny.request.bind(this.client);\r\n }\r\n return null;\r\n }\r\n\r\n private async saveRemote(\r\n threadId: string,\r\n checkpointId: string,\r\n checkpoint: Checkpoint,\r\n metadata: CheckpointMetadata\r\n ): Promise<void> {\r\n const transport = this.getTransport();\r\n if (!transport) {\r\n console.warn(\"[LetsPingCheckpointer] Missing underlying transport; falling back to in-memory only.\");\r\n return;\r\n }\r\n try {\r\n await transport(\"POST\", \"/langgraph/checkpoints\", {\r\n thread_id: threadId,\r\n checkpoint_id: checkpointId,\r\n checkpoint,\r\n metadata,\r\n });\r\n } catch (e) {\r\n console.warn(\"[LetsPingCheckpointer] Failed to persist checkpoint remotely; falling back to in-memory only.\", e);\r\n }\r\n }\r\n\r\n private async loadRemote(\r\n threadId: string,\r\n checkpointId?: string\r\n ): Promise<StoredCheckpoint | null> {\r\n const transport = this.getTransport();\r\n if (!transport) {\r\n console.warn(\"[LetsPingCheckpointer] Missing underlying transport; using in-memory checkpoints only.\");\r\n return null;\r\n }\r\n const search = checkpointId\r\n ? `?thread_id=${encodeURIComponent(threadId)}&checkpoint_id=${encodeURIComponent(checkpointId)}`\r\n : `?thread_id=${encodeURIComponent(threadId)}&latest=1`;\r\n try {\r\n const res = await transport<any>(\"GET\", `/langgraph/checkpoints${search}`);\r\n if (res && res.checkpoint && res.metadata) {\r\n return { checkpoint: res.checkpoint as Checkpoint, metadata: res.metadata as CheckpointMetadata };\r\n }\r\n } catch (e) {\r\n // If not found or backend unavailable, fall back to local cache only.\r\n console.warn(\"[LetsPingCheckpointer] Failed to load remote checkpoint\", e);\r\n }\r\n return null;\r\n }\r\n\r\n private async deleteRemote(threadId: string): Promise<void> {\r\n const transport = this.getTransport();\r\n if (!transport) return;\r\n const search = `?thread_id=${encodeURIComponent(threadId)}`;\r\n try {\r\n await transport(\"DELETE\", `/langgraph/checkpoints${search}`);\r\n } catch (e) {\r\n console.warn(\"[LetsPingCheckpointer] Failed to delete remote checkpoints\", e);\r\n }\r\n }\r\n\r\n async put(\r\n config: RunnableConfig,\r\n checkpoint: Checkpoint,\r\n metadata: CheckpointMetadata,\r\n newVersions?: Record<string, string | number>\r\n ): Promise<RunnableConfig> {\r\n const threadId = config.configurable?.thread_id;\r\n const checkpointId = checkpoint.id;\r\n\r\n if (!threadId || !checkpointId) {\r\n return config;\r\n }\r\n\r\n this.checkpoints[`${threadId}:${checkpointId}`] = { checkpoint, metadata };\r\n await this.saveRemote(threadId, checkpointId, checkpoint, metadata);\r\n\r\n return {\r\n configurable: {\r\n thread_id: threadId,\r\n checkpoint_id: checkpointId,\r\n },\r\n };\r\n }\r\n\r\n // METHODS REQUIRED BY LANGGRAPH V0.1+\r\n async putWrites(config: RunnableConfig, writes: any, taskId: string): Promise<void> {\r\n // No-op for V1: LetsPing focuses on primary state parking, not granular sub-task writes.\r\n }\r\n\r\n async deleteThread(threadId: string): Promise<void> {\r\n for (const key of Object.keys(this.checkpoints)) {\r\n if (key.startsWith(`${threadId}:`)) {\r\n delete this.checkpoints[key];\r\n }\r\n }\r\n await this.deleteRemote(threadId);\r\n }\r\n\r\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\r\n const threadId = config.configurable?.thread_id;\r\n const checkpointId = config.configurable?.checkpoint_id;\r\n if (!threadId) return undefined;\r\n\r\n // Prefer remote truth, fall back to local cache.\r\n const remote = await this.loadRemote(threadId, checkpointId);\r\n if (remote) {\r\n return { config, checkpoint: remote.checkpoint, metadata: remote.metadata };\r\n }\r\n\r\n if (checkpointId) {\r\n const match = this.checkpoints[`${threadId}:${checkpointId}`];\r\n if (match) {\r\n return { config, checkpoint: match.checkpoint, metadata: match.metadata };\r\n }\r\n }\r\n\r\n let latest: CheckpointTuple | undefined;\r\n for (const [key, val] of Object.entries(this.checkpoints)) {\r\n if (key.startsWith(`${threadId}:`)) {\r\n latest = { config, checkpoint: val.checkpoint, metadata: val.metadata };\r\n }\r\n }\r\n return latest;\r\n }\r\n\r\n async *list(config: RunnableConfig, options?: any): AsyncGenerator<CheckpointTuple> {\r\n const threadId = config.configurable?.thread_id;\r\n if (!threadId) return;\r\n\r\n const remoteLatest = await this.loadRemote(threadId);\r\n if (remoteLatest) {\r\n yield { config, checkpoint: remoteLatest.checkpoint, metadata: remoteLatest.metadata };\r\n }\r\n\r\n for (const [key, val] of Object.entries(this.checkpoints)) {\r\n if (key.startsWith(`${threadId}:`)) {\r\n yield { config, checkpoint: val.checkpoint, metadata: val.metadata };\r\n }\r\n }\r\n }\r\n}"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { StateGraph, START } from "@langchain/langgraph";
|
|
2
|
+
import { LetsPing } from "@letsping/sdk";
|
|
3
|
+
import { LetsPingCheckpointer } from "@letsping/sdk/integrations/langgraph";
|
|
4
|
+
|
|
5
|
+
type DemoState = {
|
|
6
|
+
thread_id: string;
|
|
7
|
+
step: "START" | "NEEDS_APPROVAL" | "DONE";
|
|
8
|
+
amount: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const lp = new LetsPing(process.env.LETSPING_API_KEY!);
|
|
12
|
+
const checkpointer = new LetsPingCheckpointer(lp);
|
|
13
|
+
|
|
14
|
+
// Chain the methods directly off the instantiation
|
|
15
|
+
const builder = new StateGraph<DemoState>({
|
|
16
|
+
channels: {
|
|
17
|
+
thread_id: null,
|
|
18
|
+
step: null,
|
|
19
|
+
amount: null,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
.addNode("charge_step", async (state: DemoState): Promise<DemoState> => {
|
|
23
|
+
// On the first pass, ask LetsPing for approval and park state.
|
|
24
|
+
if (state.step === "START") {
|
|
25
|
+
const decision = await lp.defer({
|
|
26
|
+
service: "demo-agent",
|
|
27
|
+
action: "payments:charge",
|
|
28
|
+
priority: "high",
|
|
29
|
+
payload: { amount: state.amount },
|
|
30
|
+
// Persist enough context so the webhook can resume the same thread.
|
|
31
|
+
state_snapshot: {
|
|
32
|
+
thread_id: state.thread_id,
|
|
33
|
+
input: state,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log("Queued LetsPing request id:", decision.id);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...state,
|
|
41
|
+
step: "NEEDS_APPROVAL",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// After approval + webhook resume, the graph will be invoked again
|
|
46
|
+
if (state.step === "NEEDS_APPROVAL") {
|
|
47
|
+
console.log("Approval received. Performing final charge for:", state.amount);
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
step: "DONE",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return state;
|
|
55
|
+
})
|
|
56
|
+
// Notice we are chaining addEdge directly after addNode
|
|
57
|
+
.addEdge(START, "charge_step");
|
|
58
|
+
|
|
59
|
+
export const demoGraph = builder.compile({ checkpointer });
|
|
60
|
+
|
|
61
|
+
if (require.main === module) {
|
|
62
|
+
(async () => {
|
|
63
|
+
const threadId = `demo-${Date.now()}`;
|
|
64
|
+
console.log("Starting demo thread:", threadId);
|
|
65
|
+
|
|
66
|
+
await demoGraph.invoke(
|
|
67
|
+
{
|
|
68
|
+
thread_id: threadId,
|
|
69
|
+
step: "START",
|
|
70
|
+
amount: 500,
|
|
71
|
+
},
|
|
72
|
+
{ configurable: { thread_id: threadId } },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
console.log("Demo graph invoked. Wait for LetsPing approval, then webhook will resume.");
|
|
76
|
+
})().catch((e) => {
|
|
77
|
+
console.error(e);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letsping/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Behavioral Firewall and Cryo-Sleep State Parking for Autonomous Agents",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"require": "./dist/index.js",
|
|
12
12
|
"import": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./integrations/langgraph": {
|
|
15
|
+
"types": "./dist/integrations/langgraph.d.ts",
|
|
16
|
+
"require": "./dist/integrations/langgraph.js",
|
|
17
|
+
"import": "./dist/integrations/langgraph.mjs"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"scripts": {
|
|
@@ -17,22 +22,31 @@
|
|
|
17
22
|
"dev": "tsup --watch",
|
|
18
23
|
"clean": "rm -rf dist .turbo"
|
|
19
24
|
},
|
|
20
|
-
"dependencies": {},
|
|
21
25
|
"peerDependencies": {
|
|
26
|
+
"@langchain/core": ">=0.1.52",
|
|
27
|
+
"@langchain/langgraph": ">=0.0.1",
|
|
22
28
|
"@opentelemetry/api": "^1.0.0"
|
|
23
29
|
},
|
|
24
30
|
"peerDependenciesMeta": {
|
|
25
31
|
"@opentelemetry/api": {
|
|
26
32
|
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"@langchain/langgraph": {
|
|
35
|
+
"optional": true
|
|
36
|
+
},
|
|
37
|
+
"@langchain/core": {
|
|
38
|
+
"optional": true
|
|
27
39
|
}
|
|
28
40
|
},
|
|
29
41
|
"devDependencies": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
42
|
+
"@langchain/core": "^1.1.28",
|
|
43
|
+
"@langchain/langgraph": "^1.1.5",
|
|
44
|
+
"@opentelemetry/api": "^1.9.0",
|
|
32
45
|
"@types/node": "^22.0.0",
|
|
33
|
-
"
|
|
46
|
+
"tsup": "^8.0.0",
|
|
47
|
+
"typescript": "^5.7.2"
|
|
34
48
|
},
|
|
35
49
|
"publishConfig": {
|
|
36
50
|
"access": "public"
|
|
37
51
|
}
|
|
38
|
-
}
|
|
52
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes, createHmac } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
let SDK_VERSION = "0.
|
|
3
|
+
let SDK_VERSION = "0.2.0";
|
|
4
4
|
try {
|
|
5
5
|
|
|
6
6
|
SDK_VERSION = require("../package.json").version;
|
|
@@ -37,6 +37,14 @@ export interface RequestOptions {
|
|
|
37
37
|
timeoutMs?: number;
|
|
38
38
|
|
|
39
39
|
role?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Optional distributed tracing identifiers. If provided, these will be
|
|
43
|
+
* attached to the request envelope so downstream frameworks can stitch
|
|
44
|
+
* together multi-agent flows.
|
|
45
|
+
*/
|
|
46
|
+
trace_id?: string;
|
|
47
|
+
parent_request_id?: string;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export interface Decision {
|
|
@@ -222,14 +230,30 @@ export class LetsPing {
|
|
|
222
230
|
});
|
|
223
231
|
}
|
|
224
232
|
|
|
233
|
+
const traceId = options.trace_id;
|
|
234
|
+
const parentId = options.parent_request_id;
|
|
235
|
+
|
|
236
|
+
// Do not mutate caller payload; attach tracing metadata under a reserved key.
|
|
237
|
+
const basePayload = options.payload || {};
|
|
238
|
+
const metaKey = "_lp_meta";
|
|
239
|
+
const existingMeta = (basePayload as any)[metaKey] || {};
|
|
240
|
+
const enrichedPayload = {
|
|
241
|
+
...basePayload,
|
|
242
|
+
[metaKey]: {
|
|
243
|
+
...existingMeta,
|
|
244
|
+
...(traceId ? { trace_id: traceId } : {}),
|
|
245
|
+
...(parentId ? { parent_request_id: parentId } : {}),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
225
249
|
try {
|
|
226
250
|
const res = await this.request<{ id: string, uploadUrl?: string, dek?: string }>("POST", "/ingest", {
|
|
227
251
|
service: options.service,
|
|
228
252
|
action: options.action,
|
|
229
|
-
payload: this._encrypt(
|
|
253
|
+
payload: this._encrypt(enrichedPayload),
|
|
230
254
|
priority: options.priority || "medium",
|
|
231
255
|
schema: options.schema,
|
|
232
|
-
metadata: { role: options.role, sdk: "node" }
|
|
256
|
+
metadata: { role: options.role, sdk: "node", trace_id: traceId, parent_request_id: parentId }
|
|
233
257
|
});
|
|
234
258
|
|
|
235
259
|
const { id, uploadUrl, dek } = res;
|
|
@@ -325,10 +349,28 @@ export class LetsPing {
|
|
|
325
349
|
});
|
|
326
350
|
}
|
|
327
351
|
|
|
352
|
+
const traceId = options.trace_id;
|
|
353
|
+
const parentId = options.parent_request_id;
|
|
354
|
+
const basePayload = options.payload || {};
|
|
355
|
+
const metaKey = "_lp_meta";
|
|
356
|
+
const existingMeta = (basePayload as any)[metaKey] || {};
|
|
357
|
+
const enrichedPayload = {
|
|
358
|
+
...basePayload,
|
|
359
|
+
[metaKey]: {
|
|
360
|
+
...existingMeta,
|
|
361
|
+
...(traceId ? { trace_id: traceId } : {}),
|
|
362
|
+
...(parentId ? { parent_request_id: parentId } : {}),
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
328
366
|
try {
|
|
329
367
|
const res = await this.request<{ id: string, uploadUrl?: string, dek?: string }>("POST", "/ingest", {
|
|
330
|
-
|
|
331
|
-
|
|
368
|
+
service: options.service,
|
|
369
|
+
action: options.action,
|
|
370
|
+
payload: this._encrypt(enrichedPayload),
|
|
371
|
+
priority: options.priority || "medium",
|
|
372
|
+
schema: options.schema,
|
|
373
|
+
metadata: { role: options.role, sdk: "node", trace_id: traceId, parent_request_id: parentId },
|
|
332
374
|
});
|
|
333
375
|
if (res.uploadUrl && options.state_snapshot) {
|
|
334
376
|
try {
|
|
@@ -436,11 +478,29 @@ export class LetsPing {
|
|
|
436
478
|
signatureHeader: string,
|
|
437
479
|
webhookSecret: string
|
|
438
480
|
): Promise<{ id: string; event: string; data: Decision; state_snapshot?: Record<string, any> }> {
|
|
439
|
-
const hmac = createHmac("sha256", webhookSecret).update(payloadStr).digest("hex");
|
|
440
481
|
const sigParts = signatureHeader.split(",").map(p => p.split("="));
|
|
441
482
|
const sigMap = Object.fromEntries(sigParts);
|
|
442
483
|
|
|
443
|
-
|
|
484
|
+
const rawTs = sigMap["t"];
|
|
485
|
+
const rawSig = sigMap["v1"];
|
|
486
|
+
if (!rawTs || !rawSig) {
|
|
487
|
+
throw new LetsPingError("LetsPing Error: Missing webhook signature fields", 401);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const ts = Number(rawTs);
|
|
491
|
+
if (!Number.isFinite(ts)) {
|
|
492
|
+
throw new LetsPingError("LetsPing Error: Invalid webhook timestamp", 401);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const now = Date.now();
|
|
496
|
+
const skewMs = Math.abs(now - ts);
|
|
497
|
+
const maxSkewMs = 5 * 60 * 1000; // 5 minutes
|
|
498
|
+
if (skewMs > maxSkewMs) {
|
|
499
|
+
throw new LetsPingError("LetsPing Error: Webhook replay window exceeded", 401);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const expected = createHmac("sha256", webhookSecret).update(payloadStr).digest("hex");
|
|
503
|
+
if (rawSig !== expected) {
|
|
444
504
|
throw new LetsPingError("LetsPing Error: Invalid webhook signature", 401);
|
|
445
505
|
}
|
|
446
506
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { BaseCheckpointSaver, Checkpoint, CheckpointMetadata, CheckpointTuple } from "@langchain/langgraph";
|
|
2
|
+
import { RunnableConfig } from "@langchain/core/runnables";
|
|
3
|
+
import { LetsPing } from "../index";
|
|
4
|
+
|
|
5
|
+
type StoredCheckpoint = {
|
|
6
|
+
checkpoint: Checkpoint;
|
|
7
|
+
metadata: CheckpointMetadata;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class LetsPingCheckpointer extends BaseCheckpointSaver {
|
|
11
|
+
private checkpoints: Record<string, StoredCheckpoint> = {};
|
|
12
|
+
|
|
13
|
+
constructor(public client: LetsPing) {
|
|
14
|
+
super();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private getTransport(): (<T = any>(method: string, path: string, body?: any) => Promise<T>) | null {
|
|
18
|
+
const clientAny = this.client as any;
|
|
19
|
+
if (typeof clientAny.request === "function") {
|
|
20
|
+
return clientAny.request.bind(this.client);
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async saveRemote(
|
|
26
|
+
threadId: string,
|
|
27
|
+
checkpointId: string,
|
|
28
|
+
checkpoint: Checkpoint,
|
|
29
|
+
metadata: CheckpointMetadata
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const transport = this.getTransport();
|
|
32
|
+
if (!transport) {
|
|
33
|
+
console.warn("[LetsPingCheckpointer] Missing underlying transport; falling back to in-memory only.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await transport("POST", "/langgraph/checkpoints", {
|
|
38
|
+
thread_id: threadId,
|
|
39
|
+
checkpoint_id: checkpointId,
|
|
40
|
+
checkpoint,
|
|
41
|
+
metadata,
|
|
42
|
+
});
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.warn("[LetsPingCheckpointer] Failed to persist checkpoint remotely; falling back to in-memory only.", e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async loadRemote(
|
|
49
|
+
threadId: string,
|
|
50
|
+
checkpointId?: string
|
|
51
|
+
): Promise<StoredCheckpoint | null> {
|
|
52
|
+
const transport = this.getTransport();
|
|
53
|
+
if (!transport) {
|
|
54
|
+
console.warn("[LetsPingCheckpointer] Missing underlying transport; using in-memory checkpoints only.");
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const search = checkpointId
|
|
58
|
+
? `?thread_id=${encodeURIComponent(threadId)}&checkpoint_id=${encodeURIComponent(checkpointId)}`
|
|
59
|
+
: `?thread_id=${encodeURIComponent(threadId)}&latest=1`;
|
|
60
|
+
try {
|
|
61
|
+
const res = await transport<any>("GET", `/langgraph/checkpoints${search}`);
|
|
62
|
+
if (res && res.checkpoint && res.metadata) {
|
|
63
|
+
return { checkpoint: res.checkpoint as Checkpoint, metadata: res.metadata as CheckpointMetadata };
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// If not found or backend unavailable, fall back to local cache only.
|
|
67
|
+
console.warn("[LetsPingCheckpointer] Failed to load remote checkpoint", e);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async deleteRemote(threadId: string): Promise<void> {
|
|
73
|
+
const transport = this.getTransport();
|
|
74
|
+
if (!transport) return;
|
|
75
|
+
const search = `?thread_id=${encodeURIComponent(threadId)}`;
|
|
76
|
+
try {
|
|
77
|
+
await transport("DELETE", `/langgraph/checkpoints${search}`);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn("[LetsPingCheckpointer] Failed to delete remote checkpoints", e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async put(
|
|
84
|
+
config: RunnableConfig,
|
|
85
|
+
checkpoint: Checkpoint,
|
|
86
|
+
metadata: CheckpointMetadata,
|
|
87
|
+
newVersions?: Record<string, string | number>
|
|
88
|
+
): Promise<RunnableConfig> {
|
|
89
|
+
const threadId = config.configurable?.thread_id;
|
|
90
|
+
const checkpointId = checkpoint.id;
|
|
91
|
+
|
|
92
|
+
if (!threadId || !checkpointId) {
|
|
93
|
+
return config;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.checkpoints[`${threadId}:${checkpointId}`] = { checkpoint, metadata };
|
|
97
|
+
await this.saveRemote(threadId, checkpointId, checkpoint, metadata);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
configurable: {
|
|
101
|
+
thread_id: threadId,
|
|
102
|
+
checkpoint_id: checkpointId,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// METHODS REQUIRED BY LANGGRAPH V0.1+
|
|
108
|
+
async putWrites(config: RunnableConfig, writes: any, taskId: string): Promise<void> {
|
|
109
|
+
// No-op for V1: LetsPing focuses on primary state parking, not granular sub-task writes.
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async deleteThread(threadId: string): Promise<void> {
|
|
113
|
+
for (const key of Object.keys(this.checkpoints)) {
|
|
114
|
+
if (key.startsWith(`${threadId}:`)) {
|
|
115
|
+
delete this.checkpoints[key];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await this.deleteRemote(threadId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {
|
|
122
|
+
const threadId = config.configurable?.thread_id;
|
|
123
|
+
const checkpointId = config.configurable?.checkpoint_id;
|
|
124
|
+
if (!threadId) return undefined;
|
|
125
|
+
|
|
126
|
+
// Prefer remote truth, fall back to local cache.
|
|
127
|
+
const remote = await this.loadRemote(threadId, checkpointId);
|
|
128
|
+
if (remote) {
|
|
129
|
+
return { config, checkpoint: remote.checkpoint, metadata: remote.metadata };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (checkpointId) {
|
|
133
|
+
const match = this.checkpoints[`${threadId}:${checkpointId}`];
|
|
134
|
+
if (match) {
|
|
135
|
+
return { config, checkpoint: match.checkpoint, metadata: match.metadata };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let latest: CheckpointTuple | undefined;
|
|
140
|
+
for (const [key, val] of Object.entries(this.checkpoints)) {
|
|
141
|
+
if (key.startsWith(`${threadId}:`)) {
|
|
142
|
+
latest = { config, checkpoint: val.checkpoint, metadata: val.metadata };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return latest;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async *list(config: RunnableConfig, options?: any): AsyncGenerator<CheckpointTuple> {
|
|
149
|
+
const threadId = config.configurable?.thread_id;
|
|
150
|
+
if (!threadId) return;
|
|
151
|
+
|
|
152
|
+
const remoteLatest = await this.loadRemote(threadId);
|
|
153
|
+
if (remoteLatest) {
|
|
154
|
+
yield { config, checkpoint: remoteLatest.checkpoint, metadata: remoteLatest.metadata };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const [key, val] of Object.entries(this.checkpoints)) {
|
|
158
|
+
if (key.startsWith(`${threadId}:`)) {
|
|
159
|
+
yield { config, checkpoint: val.checkpoint, metadata: val.metadata };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|