@letsping/sdk 0.1.6 → 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.
@@ -2,13 +2,84 @@ import { BaseCheckpointSaver, Checkpoint, CheckpointMetadata, CheckpointTuple }
2
2
  import { RunnableConfig } from "@langchain/core/runnables";
3
3
  import { LetsPing } from "../index";
4
4
 
5
+ type StoredCheckpoint = {
6
+ checkpoint: Checkpoint;
7
+ metadata: CheckpointMetadata;
8
+ };
9
+
5
10
  export class LetsPingCheckpointer extends BaseCheckpointSaver {
6
- private checkpoints: Record<string, [Checkpoint, CheckpointMetadata]> = {};
11
+ private checkpoints: Record<string, StoredCheckpoint> = {};
7
12
 
8
13
  constructor(public client: LetsPing) {
9
14
  super();
10
15
  }
11
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
+
12
83
  async put(
13
84
  config: RunnableConfig,
14
85
  checkpoint: Checkpoint,
@@ -18,17 +89,22 @@ export class LetsPingCheckpointer extends BaseCheckpointSaver {
18
89
  const threadId = config.configurable?.thread_id;
19
90
  const checkpointId = checkpoint.id;
20
91
 
21
- this.checkpoints[`${threadId}:${checkpointId}`] = [checkpoint, metadata];
92
+ if (!threadId || !checkpointId) {
93
+ return config;
94
+ }
95
+
96
+ this.checkpoints[`${threadId}:${checkpointId}`] = { checkpoint, metadata };
97
+ await this.saveRemote(threadId, checkpointId, checkpoint, metadata);
22
98
 
23
99
  return {
24
100
  configurable: {
25
101
  thread_id: threadId,
26
102
  checkpoint_id: checkpointId,
27
- }
103
+ },
28
104
  };
29
105
  }
30
106
 
31
- // --- NEW METHODS REQUIRED BY LANGGRAPH V0.1+ ---
107
+ // METHODS REQUIRED BY LANGGRAPH V0.1+
32
108
  async putWrites(config: RunnableConfig, writes: any, taskId: string): Promise<void> {
33
109
  // No-op for V1: LetsPing focuses on primary state parking, not granular sub-task writes.
34
110
  }
@@ -39,27 +115,49 @@ export class LetsPingCheckpointer extends BaseCheckpointSaver {
39
115
  delete this.checkpoints[key];
40
116
  }
41
117
  }
118
+ await this.deleteRemote(threadId);
42
119
  }
43
120
 
44
121
  async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {
45
122
  const threadId = config.configurable?.thread_id;
46
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
+ }
47
131
 
48
132
  if (checkpointId) {
49
133
  const match = this.checkpoints[`${threadId}:${checkpointId}`];
50
- if (match) return { config, checkpoint: match[0], metadata: match[1] };
134
+ if (match) {
135
+ return { config, checkpoint: match.checkpoint, metadata: match.metadata };
136
+ }
51
137
  }
52
138
 
53
139
  let latest: CheckpointTuple | undefined;
54
140
  for (const [key, val] of Object.entries(this.checkpoints)) {
55
141
  if (key.startsWith(`${threadId}:`)) {
56
- latest = { config, checkpoint: val[0], metadata: val[1] };
142
+ latest = { config, checkpoint: val.checkpoint, metadata: val.metadata };
57
143
  }
58
144
  }
59
145
  return latest;
60
146
  }
61
147
 
62
148
  async *list(config: RunnableConfig, options?: any): AsyncGenerator<CheckpointTuple> {
63
- yield* [];
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
+ }
64
162
  }
65
163
  }