@gaunt-sloth/core 2.0.0-beta.5 → 2.0.0-beta.6
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/dist/config/schema.d.ts +4 -0
- package/dist/config/schema.js +64 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/config/tokenBudget.d.ts +88 -0
- package/dist/config/tokenBudget.js +155 -0
- package/dist/config/tokenBudget.js.map +1 -0
- package/dist/config/types.d.ts +33 -4
- package/dist/config/types.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +24 -0
- package/dist/core/GthAbstractAgent.js +37 -1
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +216 -1
- package/dist/core/GthAgentRunner.js +424 -3
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +196 -0
- package/dist/core/GthLangChainAgent.js +392 -2
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/conversationGrants.d.ts +60 -0
- package/dist/core/approvals/conversationGrants.js +77 -0
- package/dist/core/approvals/conversationGrants.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +16 -0
- package/dist/core/approvals/grants.js +20 -5
- package/dist/core/approvals/grants.js.map +1 -1
- package/dist/core/compaction.d.ts +181 -0
- package/dist/core/compaction.js +293 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/compactionThreshold.d.ts +158 -0
- package/dist/core/compactionThreshold.js +183 -0
- package/dist/core/compactionThreshold.js.map +1 -0
- package/dist/core/contextWindow.d.ts +146 -0
- package/dist/core/contextWindow.js +256 -0
- package/dist/core/contextWindow.js.map +1 -0
- package/dist/core/exitOutputChannel.d.ts +51 -0
- package/dist/core/exitOutputChannel.js +65 -0
- package/dist/core/exitOutputChannel.js.map +1 -0
- package/dist/core/refusal.d.ts +17 -2
- package/dist/core/refusal.js +80 -14
- package/dist/core/refusal.js.map +1 -1
- package/dist/core/runStats.d.ts +1 -1
- package/dist/core/terminationNotice.d.ts +8 -0
- package/dist/core/terminationNotice.js +10 -4
- package/dist/core/terminationNotice.js.map +1 -1
- package/dist/core/terminationReason.d.ts +28 -0
- package/dist/core/terminationReason.js +27 -0
- package/dist/core/terminationReason.js.map +1 -1
- package/dist/core/types.d.ts +35 -1
- package/dist/core/types.js.map +1 -1
- package/dist/history/checkpointRetention.d.ts +279 -0
- package/dist/history/checkpointRetention.js +567 -0
- package/dist/history/checkpointRetention.js.map +1 -0
- package/dist/history/checkpointSaver.d.ts +93 -0
- package/dist/history/checkpointSaver.js +464 -0
- package/dist/history/checkpointSaver.js.map +1 -0
- package/dist/history/historyEnabled.d.ts +27 -0
- package/dist/history/historyEnabled.js +23 -0
- package/dist/history/historyEnabled.js.map +1 -0
- package/dist/history/historyFormat.d.ts +27 -0
- package/dist/history/historyFormat.js +125 -2
- package/dist/history/historyFormat.js.map +1 -1
- package/dist/history/historyStore.d.ts +61 -0
- package/dist/history/historyStore.js +180 -7
- package/dist/history/historyStore.js.map +1 -1
- package/dist/history/recordSession.d.ts +84 -22
- package/dist/history/recordSession.js +187 -12
- package/dist/history/recordSession.js.map +1 -1
- package/dist/history/sessionCheckpointer.d.ts +48 -0
- package/dist/history/sessionCheckpointer.js +200 -0
- package/dist/history/sessionCheckpointer.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/modelCatalog.d.ts +14 -0
- package/dist/providers/modelCatalog.js +4 -0
- package/dist/providers/modelCatalog.js.map +1 -1
- package/dist/providers/modelDiscovery.d.ts +3 -1
- package/dist/providers/modelDiscovery.js +22 -8
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.js +3 -19
- package/dist/providers/ollama.js.map +1 -1
- package/dist/runtime/conversation.js +7 -1
- package/dist/runtime/conversation.js.map +1 -1
- package/dist/runtime/singleShot.js +6 -1
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +77 -0
- package/dist/utils/consoleUtils.js +81 -0
- package/dist/utils/consoleUtils.js.map +1 -1
- package/package.json +2 -2
- package/schema/gsloth-config.schema.json +33 -14
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
2
|
+
import type { Checkpoint, CheckpointMetadata, CheckpointTuple } from '@langchain/langgraph';
|
|
3
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
4
|
+
import { type CheckpointStoreStats, type ReclaimSummary } from '#src/history/checkpointRetention.js';
|
|
5
|
+
/**
|
|
6
|
+
* The abstract members' own parameter types, read off the base class rather than imported.
|
|
7
|
+
* `@langchain/langgraph` re-exports `BaseCheckpointSaver` (and `Checkpoint`, `CheckpointMetadata`,
|
|
8
|
+
* `CheckpointTuple`) but not `ChannelVersions`, `CheckpointListOptions` or `PendingWrite`, which
|
|
9
|
+
* live only in `@langchain/langgraph-checkpoint` — a package this one does not depend on. Deriving
|
|
10
|
+
* them keeps the signatures exactly the base class's without adding a dependency whose second
|
|
11
|
+
* physical copy could disagree with the one LangGraph itself loads. `put`'s fourth parameter is
|
|
12
|
+
* derived inline at its signature for the same reason, kept out of this list so the alias does not
|
|
13
|
+
* surface as an undocumented type in the rendered API reference.
|
|
14
|
+
*/
|
|
15
|
+
type ListOptions = NonNullable<Parameters<BaseCheckpointSaver['list']>[1]>;
|
|
16
|
+
type PendingWrites = Parameters<BaseCheckpointSaver['putWrites']>[1];
|
|
17
|
+
/** Options for {@link openCheckpointSaver}. */
|
|
18
|
+
export interface CheckpointSaverOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Called when a `put` / `putWrites` could not reach the database. The write is dropped and the
|
|
21
|
+
* turn continues; the handler's job is to tell the user and to record that this conversation can
|
|
22
|
+
* no longer be resumed. Called on EVERY failed write — the once-per-session rule belongs to the
|
|
23
|
+
* handler, which is the layer that knows what a session is.
|
|
24
|
+
*/
|
|
25
|
+
onWriteFailure?: (error: unknown) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A LangGraph `BaseCheckpointSaver` persisting to a `node:sqlite` database.
|
|
29
|
+
*
|
|
30
|
+
* Obtain one through {@link openCheckpointSaver}, which is fail-soft; the constructor is private so
|
|
31
|
+
* there is no way to hold one whose tables were never created.
|
|
32
|
+
*/
|
|
33
|
+
export declare class GthSqliteSaver extends BaseCheckpointSaver {
|
|
34
|
+
private db;
|
|
35
|
+
private onWriteFailure;
|
|
36
|
+
/**
|
|
37
|
+
* GS2-107 — every thread this saver has written to, which is what automatic reclamation excludes.
|
|
38
|
+
*
|
|
39
|
+
* The thread a session writes to is not fixed at open: `resetThread()` mints a fresh one on
|
|
40
|
+
* `/clear` and `resumeConversation` rebinds onto a stored one, and neither tells this object. An
|
|
41
|
+
* exclusion built from the id the session started with therefore names a thread nobody is writing
|
|
42
|
+
* and misses the one that is — which on the `/clear` path is a thread no conversation row names,
|
|
43
|
+
* i.e. exactly a reclamation candidate. Recording the ids as they arrive needs no notification at
|
|
44
|
+
* all: whatever the runner rotated onto, the write came through here.
|
|
45
|
+
*/
|
|
46
|
+
private readonly writtenThreads;
|
|
47
|
+
private constructor();
|
|
48
|
+
/**
|
|
49
|
+
* Report a dropped write, and never let the reporting itself become the failure. A handler that
|
|
50
|
+
* throws here would land back inside `agent.invoke()` and end the turn — the exact outcome the
|
|
51
|
+
* degrade posture exists to avoid, arriving from the code that implements it.
|
|
52
|
+
*/
|
|
53
|
+
private reportWriteFailure;
|
|
54
|
+
/**
|
|
55
|
+
* Open (and lazily initialise) the checkpoint tables at `dbPath`. Returns `null` on any failure —
|
|
56
|
+
* an unopenable, read-only or corrupt DB — so the caller can fall back to a `MemorySaver` without
|
|
57
|
+
* a try/catch of its own.
|
|
58
|
+
*/
|
|
59
|
+
static open(dbPath: string, options?: CheckpointSaverOptions): GthSqliteSaver | null;
|
|
60
|
+
/** Close the underlying connection (fail-soft). */
|
|
61
|
+
close(): void;
|
|
62
|
+
/** Rebuild one stored row into the tuple LangGraph reads, pending writes and parent included. */
|
|
63
|
+
private toTuple;
|
|
64
|
+
getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;
|
|
65
|
+
list(config: RunnableConfig, options?: ListOptions): AsyncGenerator<CheckpointTuple>;
|
|
66
|
+
put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: Parameters<BaseCheckpointSaver['put']>[3]): Promise<RunnableConfig>;
|
|
67
|
+
putWrites(config: RunnableConfig, writes: PendingWrites, taskId: string): Promise<void>;
|
|
68
|
+
deleteThread(threadId: string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* GS2-107 — delete every thread no conversation row names, past the grace window. The retention
|
|
71
|
+
* module owns the policy and the reasoning; this is the saver's own connection lent to it, so a
|
|
72
|
+
* session that already has the store open can reclaim without opening it again.
|
|
73
|
+
*
|
|
74
|
+
* **Every thread this saver wrote is excluded, always**, on top of whatever the caller names. A
|
|
75
|
+
* caller cannot supply that set: the runner rotates threads without telling anyone, so the only
|
|
76
|
+
* place that knows which thread the session ended on is the object the writes went through. The
|
|
77
|
+
* union is the contract — `excludeThreadIds` adds to the protection and can never subtract from
|
|
78
|
+
* it.
|
|
79
|
+
*/
|
|
80
|
+
reclaimUnresumableThreads(options?: {
|
|
81
|
+
now?: number;
|
|
82
|
+
graceMs?: number;
|
|
83
|
+
excludeThreadIds?: readonly string[];
|
|
84
|
+
}): ReclaimSummary;
|
|
85
|
+
/** GS2-107 — what the checkpoint tables hold, over this saver's open connection. */
|
|
86
|
+
storeStats(dbPath: string, topN?: number): CheckpointStoreStats;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Fail-soft open of the durable checkpoint saver. Returns `null` (never throws) when the DB cannot
|
|
90
|
+
* be opened or its tables cannot be created.
|
|
91
|
+
*/
|
|
92
|
+
export declare function openCheckpointSaver(dbPath: string, options?: CheckpointSaverOptions): GthSqliteSaver | null;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-20 — a **durable LangGraph checkpointer** over the same local SQLite file the session
|
|
4
|
+
* history store uses, so a past conversation can be resumed with the graph state it actually had.
|
|
5
|
+
*
|
|
6
|
+
* Resuming by replaying stored messages into a fresh agent rebuilds a transcript and discards
|
|
7
|
+
* everything the LangGraph graph was holding — pending writes, a suspended interrupt, the channel
|
|
8
|
+
* versions that make `add_messages` reconcile rather than append. What a resume has to produce is
|
|
9
|
+
* that, from the agent's side, no interruption happened, and only a saver whose checkpoints outlive
|
|
10
|
+
* the process can produce it.
|
|
11
|
+
*
|
|
12
|
+
* **Written against the built-in `node:sqlite`, deliberately.** Gaunt Sloth is installed globally
|
|
13
|
+
* with `npm i -g`, and the history store (`historyStore.ts`) states the constraint this inherits:
|
|
14
|
+
* zero native dependency, no build step. The published LangGraph SQLite savers are built on
|
|
15
|
+
* `better-sqlite3`, a native module with a compile step, which would put a node-gyp build in the
|
|
16
|
+
* path of every install of the CLI.
|
|
17
|
+
*
|
|
18
|
+
* **Storage layout.** Two tables of this module's own — `checkpoints` and `checkpoint_writes` —
|
|
19
|
+
* created idempotently on open, in the same file as the store's `sessions` / `conversations`.
|
|
20
|
+
* One file, one opt-out, one thing to delete. The store owns its tables and this owns these; there
|
|
21
|
+
* is no shared DDL, so the store's per-call open (which re-runs its own migration every time)
|
|
22
|
+
* cannot race this module's long-lived connection over the same schema.
|
|
23
|
+
*
|
|
24
|
+
* **Failure posture: degrade, loudly.** {@link openCheckpointSaver} is fail-soft — it returns `null`
|
|
25
|
+
* rather than throwing when the DB cannot be opened or the tables cannot be created, which is what
|
|
26
|
+
* lets the session fall back to a `MemorySaver` (see `openSessionCheckpointerSafe` in
|
|
27
|
+
* `sessionCheckpointer.ts`). A write that fails AFTER that — a full disk, a filesystem that went
|
|
28
|
+
* read-only under a live handle — is caught here and reported through
|
|
29
|
+
* {@link CheckpointSaverOptions.onWriteFailure} instead of propagating.
|
|
30
|
+
*
|
|
31
|
+
* Neither of the two obvious postures is right, and the reason is worth keeping. **Swallowing**
|
|
32
|
+
* would report a resumed session as empty and turn a suspended tool approval into a graph nobody
|
|
33
|
+
* can resume — silent and wrong. **Throwing** propagates out of `agent.invoke()` into the session's
|
|
34
|
+
* outer catch and ends the session, which since GS2-20 made history the default would mean a full
|
|
35
|
+
* disk takes down the live session of a user who never asked for the feature; recording a
|
|
36
|
+
* conversation must not become a new way to lose one. So the third posture: the write is dropped,
|
|
37
|
+
* the turn continues, the user is told once, and the conversation is marked **unresumable on disk**
|
|
38
|
+
* — that last part is what stops a silent drop from becoming a resume of a truncated conversation,
|
|
39
|
+
* and it is why this differs from the history RECORDER, which merely swallows. A missing turn in a
|
|
40
|
+
* listing is a gap; a half-restored graph presented as whole is a lie.
|
|
41
|
+
*
|
|
42
|
+
* Reads are still loud: nothing is degraded by a failed `getTuple`, and a resume that cannot read
|
|
43
|
+
* must not pretend the thread was empty.
|
|
44
|
+
*/
|
|
45
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
46
|
+
import { BaseCheckpointSaver, copyCheckpoint } from '@langchain/langgraph';
|
|
47
|
+
import { collectCheckpointStoreStats, deleteThreads, reclaimUnresumableThreads, } from '#src/history/checkpointRetention.js';
|
|
48
|
+
/**
|
|
49
|
+
* LangGraph's reserved channel names and the fixed NEGATIVE slot each takes in a task's writes.
|
|
50
|
+
*
|
|
51
|
+
* A saver's `putWrites` is insert-once for ordinary writes (a retried super-step must not duplicate
|
|
52
|
+
* them) but replace-on-write for these four, which is why they need slots that cannot collide with
|
|
53
|
+
* a positional index. `__interrupt__` and `__resume__` are the pair the tool-approval gate rides on,
|
|
54
|
+
* so getting this wrong is not academic: a stale `__interrupt__` write kept beside a fresh one is a
|
|
55
|
+
* suspended approval that resumes twice.
|
|
56
|
+
*
|
|
57
|
+
* Mirrors `WRITES_IDX_MAP` in `@langchain/langgraph-checkpoint`, which does not re-export through
|
|
58
|
+
* `@langchain/langgraph`. Copied rather than depended on — see the note on the derived types above.
|
|
59
|
+
* A reserved name added upstream and missed here degrades to a positional slot: that write becomes
|
|
60
|
+
* insert-once instead of replace, which is visible only on a resume, so re-check this list when the
|
|
61
|
+
* LangGraph major moves.
|
|
62
|
+
*/
|
|
63
|
+
const WRITES_IDX_MAP = Object.freeze({
|
|
64
|
+
__error__: -1,
|
|
65
|
+
__scheduled__: -2,
|
|
66
|
+
__interrupt__: -3,
|
|
67
|
+
__resume__: -4,
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* How long a statement waits for another connection's lock before giving up. The history recorder
|
|
71
|
+
* opens the same file for a moment at the end of every turn, so two writers on one file is the
|
|
72
|
+
* ordinary case here, not the exceptional one; without this a routine overlap surfaces as a
|
|
73
|
+
* `SQLITE_BUSY` that aborts a turn.
|
|
74
|
+
*/
|
|
75
|
+
const BUSY_TIMEOUT_MS = 5000;
|
|
76
|
+
/** The checkpoint id named by a config, using LangGraph's own precedence (`thread_ts` is legacy). */
|
|
77
|
+
function checkpointIdOf(config) {
|
|
78
|
+
const configurable = config.configurable;
|
|
79
|
+
const id = configurable?.checkpoint_id ?? configurable?.thread_ts;
|
|
80
|
+
return typeof id === 'string' ? id : '';
|
|
81
|
+
}
|
|
82
|
+
/** Read a `configurable` field as a string, or `undefined` when it is absent / not a string. */
|
|
83
|
+
function stringField(config, key) {
|
|
84
|
+
const value = config.configurable?.[key];
|
|
85
|
+
return typeof value === 'string' ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Narrow one raw SQLite row to {@link CheckpointRow}. Written out rather than cast because
|
|
89
|
+
* `node:sqlite` types every column as `SQLOutputValue`, so a blanket assertion would also hide a
|
|
90
|
+
* genuine column/shape mismatch — including the BLOB columns, which are the ones a wrong value
|
|
91
|
+
* would break silently inside the deserializer rather than here.
|
|
92
|
+
*/
|
|
93
|
+
function toCheckpointRow(row) {
|
|
94
|
+
return {
|
|
95
|
+
thread_id: String(row.thread_id),
|
|
96
|
+
checkpoint_ns: row.checkpoint_ns != null ? String(row.checkpoint_ns) : '',
|
|
97
|
+
checkpoint_id: String(row.checkpoint_id),
|
|
98
|
+
parent_checkpoint_id: row.parent_checkpoint_id != null ? String(row.parent_checkpoint_id) : null,
|
|
99
|
+
type: row.type != null ? String(row.type) : null,
|
|
100
|
+
checkpoint: row.checkpoint,
|
|
101
|
+
metadata: row.metadata,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A LangGraph `BaseCheckpointSaver` persisting to a `node:sqlite` database.
|
|
106
|
+
*
|
|
107
|
+
* Obtain one through {@link openCheckpointSaver}, which is fail-soft; the constructor is private so
|
|
108
|
+
* there is no way to hold one whose tables were never created.
|
|
109
|
+
*/
|
|
110
|
+
export class GthSqliteSaver extends BaseCheckpointSaver {
|
|
111
|
+
db;
|
|
112
|
+
onWriteFailure;
|
|
113
|
+
/**
|
|
114
|
+
* GS2-107 — every thread this saver has written to, which is what automatic reclamation excludes.
|
|
115
|
+
*
|
|
116
|
+
* The thread a session writes to is not fixed at open: `resetThread()` mints a fresh one on
|
|
117
|
+
* `/clear` and `resumeConversation` rebinds onto a stored one, and neither tells this object. An
|
|
118
|
+
* exclusion built from the id the session started with therefore names a thread nobody is writing
|
|
119
|
+
* and misses the one that is — which on the `/clear` path is a thread no conversation row names,
|
|
120
|
+
* i.e. exactly a reclamation candidate. Recording the ids as they arrive needs no notification at
|
|
121
|
+
* all: whatever the runner rotated onto, the write came through here.
|
|
122
|
+
*/
|
|
123
|
+
writtenThreads = new Set();
|
|
124
|
+
constructor(db, onWriteFailure) {
|
|
125
|
+
super();
|
|
126
|
+
this.db = db;
|
|
127
|
+
this.onWriteFailure = onWriteFailure ?? (() => { });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Report a dropped write, and never let the reporting itself become the failure. A handler that
|
|
131
|
+
* throws here would land back inside `agent.invoke()` and end the turn — the exact outcome the
|
|
132
|
+
* degrade posture exists to avoid, arriving from the code that implements it.
|
|
133
|
+
*/
|
|
134
|
+
reportWriteFailure(error) {
|
|
135
|
+
try {
|
|
136
|
+
this.onWriteFailure(error);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* ignore */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Open (and lazily initialise) the checkpoint tables at `dbPath`. Returns `null` on any failure —
|
|
144
|
+
* an unopenable, read-only or corrupt DB — so the caller can fall back to a `MemorySaver` without
|
|
145
|
+
* a try/catch of its own.
|
|
146
|
+
*/
|
|
147
|
+
static open(dbPath, options = {}) {
|
|
148
|
+
let db;
|
|
149
|
+
try {
|
|
150
|
+
db = new DatabaseSync(dbPath);
|
|
151
|
+
// GS2-42's lesson applied here too: SQLite does not validate the file header until the first
|
|
152
|
+
// statement runs, so a garbage file opens cleanly and fails on the DDL below. Everything from
|
|
153
|
+
// the constructor to the last `exec` is therefore inside one try, and the handle is closed on
|
|
154
|
+
// the way out — on win32 an unclosed handle blocks the file from being replaced or reopened
|
|
155
|
+
// until the process exits.
|
|
156
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
157
|
+
db.exec(`
|
|
158
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
159
|
+
thread_id TEXT NOT NULL,
|
|
160
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
161
|
+
checkpoint_id TEXT NOT NULL,
|
|
162
|
+
parent_checkpoint_id TEXT,
|
|
163
|
+
type TEXT,
|
|
164
|
+
checkpoint BLOB,
|
|
165
|
+
metadata BLOB,
|
|
166
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
|
|
167
|
+
);
|
|
168
|
+
CREATE TABLE IF NOT EXISTS checkpoint_writes (
|
|
169
|
+
thread_id TEXT NOT NULL,
|
|
170
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
171
|
+
checkpoint_id TEXT NOT NULL,
|
|
172
|
+
task_id TEXT NOT NULL,
|
|
173
|
+
idx INTEGER NOT NULL,
|
|
174
|
+
channel TEXT NOT NULL,
|
|
175
|
+
type TEXT,
|
|
176
|
+
value BLOB,
|
|
177
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
|
178
|
+
);
|
|
179
|
+
`);
|
|
180
|
+
return new GthSqliteSaver(db, options.onWriteFailure);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
try {
|
|
184
|
+
db?.close();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
/* ignore: the connection may already be in a broken state */
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Close the underlying connection (fail-soft). */
|
|
193
|
+
close() {
|
|
194
|
+
try {
|
|
195
|
+
this.db.close();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* ignore */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Rebuild one stored row into the tuple LangGraph reads, pending writes and parent included. */
|
|
202
|
+
async toTuple(row, config) {
|
|
203
|
+
const type = row.type ?? 'json';
|
|
204
|
+
const checkpoint = (await this.serde.loadsTyped(type, row.checkpoint));
|
|
205
|
+
const metadata = (await this.serde.loadsTyped(type, row.metadata));
|
|
206
|
+
const writeRows = this.db
|
|
207
|
+
.prepare(`SELECT task_id, channel, type, value
|
|
208
|
+
FROM checkpoint_writes
|
|
209
|
+
WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?
|
|
210
|
+
ORDER BY task_id ASC, idx ASC`)
|
|
211
|
+
.all(row.thread_id, row.checkpoint_ns, row.checkpoint_id);
|
|
212
|
+
const pendingWrites = [];
|
|
213
|
+
for (const w of writeRows) {
|
|
214
|
+
pendingWrites.push([
|
|
215
|
+
String(w.task_id),
|
|
216
|
+
String(w.channel),
|
|
217
|
+
await this.serde.loadsTyped(String(w.type ?? 'json'), w.value),
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
const tuple = { config, checkpoint, metadata, pendingWrites };
|
|
221
|
+
// The parent link is part of the interface, not optional decoration:
|
|
222
|
+
// `BaseCheckpointSaver.getDeltaChannelHistory` reconstructs a DELTA channel by walking
|
|
223
|
+
// `getTuple` → `parentConfig` up the ancestor chain, and a tuple without it terminates that walk
|
|
224
|
+
// at the first checkpoint.
|
|
225
|
+
//
|
|
226
|
+
// **What that costs depends on the state schema, and on THIS graph it costs less than it looks.**
|
|
227
|
+
// The agent's `messages` channel stores a full array in `channel_values`, not a delta, so every
|
|
228
|
+
// checkpoint already carries the whole transcript and a broken walk cannot produce the
|
|
229
|
+
// "transcript present, tool result missing" shape. Dropping the link here reddens the
|
|
230
|
+
// parent-link test and nothing else — measured, not assumed. The reason to keep returning it is
|
|
231
|
+
// the conditional one: a state schema that puts any channel behind a binary operator (a reducer
|
|
232
|
+
// accumulating deltas, which `add_messages` is NOT under the current serialization) would
|
|
233
|
+
// reconstruct that channel from the ancestor chain, and then a missing parent silently seeds it
|
|
234
|
+
// empty. Correct now, and load-bearing the moment the schema changes.
|
|
235
|
+
if (row.parent_checkpoint_id != null) {
|
|
236
|
+
tuple.parentConfig = {
|
|
237
|
+
configurable: {
|
|
238
|
+
thread_id: row.thread_id,
|
|
239
|
+
checkpoint_ns: row.checkpoint_ns,
|
|
240
|
+
checkpoint_id: row.parent_checkpoint_id,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return tuple;
|
|
245
|
+
}
|
|
246
|
+
async getTuple(config) {
|
|
247
|
+
const threadId = stringField(config, 'thread_id');
|
|
248
|
+
if (threadId === undefined)
|
|
249
|
+
return undefined;
|
|
250
|
+
const checkpointNs = stringField(config, 'checkpoint_ns') ?? '';
|
|
251
|
+
const checkpointId = checkpointIdOf(config);
|
|
252
|
+
// Checkpoint ids are uuid6, which sort lexicographically in creation order, so "the latest
|
|
253
|
+
// checkpoint on this thread" is a plain DESC on the id — the same ordering MemorySaver gets
|
|
254
|
+
// from sorting its keys.
|
|
255
|
+
const raw = (checkpointId
|
|
256
|
+
? this.db
|
|
257
|
+
.prepare(`SELECT * FROM checkpoints
|
|
258
|
+
WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?`)
|
|
259
|
+
.get(threadId, checkpointNs, checkpointId)
|
|
260
|
+
: this.db
|
|
261
|
+
.prepare(`SELECT * FROM checkpoints
|
|
262
|
+
WHERE thread_id = ? AND checkpoint_ns = ?
|
|
263
|
+
ORDER BY checkpoint_id DESC
|
|
264
|
+
LIMIT 1`)
|
|
265
|
+
.get(threadId, checkpointNs));
|
|
266
|
+
if (raw === undefined)
|
|
267
|
+
return undefined;
|
|
268
|
+
const row = toCheckpointRow(raw);
|
|
269
|
+
// When the caller named a checkpoint, echo their config back (they may carry other
|
|
270
|
+
// `configurable` fields); when they asked for the latest, name the one we actually found.
|
|
271
|
+
const tupleConfig = checkpointId
|
|
272
|
+
? config
|
|
273
|
+
: {
|
|
274
|
+
configurable: {
|
|
275
|
+
thread_id: threadId,
|
|
276
|
+
checkpoint_ns: checkpointNs,
|
|
277
|
+
checkpoint_id: row.checkpoint_id,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
return this.toTuple(row, tupleConfig);
|
|
281
|
+
}
|
|
282
|
+
async *list(config, options) {
|
|
283
|
+
const { before, limit, filter } = options ?? {};
|
|
284
|
+
const clauses = [];
|
|
285
|
+
const params = [];
|
|
286
|
+
const threadId = stringField(config, 'thread_id');
|
|
287
|
+
if (threadId !== undefined) {
|
|
288
|
+
clauses.push('thread_id = ?');
|
|
289
|
+
params.push(threadId);
|
|
290
|
+
}
|
|
291
|
+
const checkpointNs = stringField(config, 'checkpoint_ns');
|
|
292
|
+
if (checkpointNs !== undefined) {
|
|
293
|
+
clauses.push('checkpoint_ns = ?');
|
|
294
|
+
params.push(checkpointNs);
|
|
295
|
+
}
|
|
296
|
+
const checkpointId = checkpointIdOf(config);
|
|
297
|
+
if (checkpointId) {
|
|
298
|
+
clauses.push('checkpoint_id = ?');
|
|
299
|
+
params.push(checkpointId);
|
|
300
|
+
}
|
|
301
|
+
const beforeId = before ? checkpointIdOf(before) : '';
|
|
302
|
+
if (beforeId) {
|
|
303
|
+
clauses.push('checkpoint_id < ?');
|
|
304
|
+
params.push(beforeId);
|
|
305
|
+
}
|
|
306
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
307
|
+
const rows = this.db
|
|
308
|
+
.prepare(`SELECT * FROM checkpoints
|
|
309
|
+
${where}
|
|
310
|
+
ORDER BY thread_id ASC, checkpoint_ns ASC, checkpoint_id DESC`)
|
|
311
|
+
.all(...params).map(toCheckpointRow);
|
|
312
|
+
// `limit` counts rows that SURVIVE `filter`, so it is applied here rather than as SQL LIMIT —
|
|
313
|
+
// a metadata filter is evaluated on the deserialized object, which SQL cannot see.
|
|
314
|
+
let remaining = limit;
|
|
315
|
+
for (const row of rows) {
|
|
316
|
+
if (remaining !== undefined && remaining <= 0)
|
|
317
|
+
return;
|
|
318
|
+
const tuple = await this.toTuple(row, {
|
|
319
|
+
configurable: {
|
|
320
|
+
thread_id: row.thread_id,
|
|
321
|
+
checkpoint_ns: row.checkpoint_ns,
|
|
322
|
+
checkpoint_id: row.checkpoint_id,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
if (filter) {
|
|
326
|
+
const metadata = (tuple.metadata ?? {});
|
|
327
|
+
if (!Object.entries(filter).every(([key, value]) => metadata[key] === value))
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (remaining !== undefined)
|
|
331
|
+
remaining -= 1;
|
|
332
|
+
yield tuple;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async put(config, checkpoint, metadata, _newVersions) {
|
|
336
|
+
const threadId = stringField(config, 'thread_id');
|
|
337
|
+
if (threadId === undefined) {
|
|
338
|
+
throw new Error('Failed to put checkpoint: the RunnableConfig is missing a "thread_id" in its ' +
|
|
339
|
+
'"configurable" property. A checkpointer needs a thread_id to know which conversation ' +
|
|
340
|
+
'to persist state for.');
|
|
341
|
+
}
|
|
342
|
+
// Recorded BEFORE the write is attempted, and kept even when it fails. A thread whose write was
|
|
343
|
+
// dropped is one the degrade path has just marked unresumable, and a live session is still
|
|
344
|
+
// sitting on it; excluding it costs a delete that the next process's pass will make anyway.
|
|
345
|
+
this.writtenThreads.add(threadId);
|
|
346
|
+
const checkpointNs = stringField(config, 'checkpoint_ns') ?? '';
|
|
347
|
+
try {
|
|
348
|
+
const [checkpointType, serializedCheckpoint] = await this.serde.dumpsTyped(copyCheckpoint(checkpoint));
|
|
349
|
+
const [metadataType, serializedMetadata] = await this.serde.dumpsTyped(metadata);
|
|
350
|
+
if (checkpointType !== metadataType) {
|
|
351
|
+
throw new Error('Failed to serialize the checkpoint and its metadata to the same type.');
|
|
352
|
+
}
|
|
353
|
+
this.db
|
|
354
|
+
.prepare(`INSERT OR REPLACE INTO checkpoints
|
|
355
|
+
(thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata)
|
|
356
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
357
|
+
.run(threadId, checkpointNs, checkpoint.id, checkpointIdOf(config) || null, checkpointType, serializedCheckpoint, serializedMetadata);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
this.reportWriteFailure(error);
|
|
361
|
+
}
|
|
362
|
+
// Returned whether or not the row landed, and that is not an oversight. LangGraph takes this
|
|
363
|
+
// config as the parent of the NEXT super-step, so a failure here leaves the next successful
|
|
364
|
+
// write pointing at a `parent_checkpoint_id` that was never stored — a chain with a hole in it.
|
|
365
|
+
// What makes that safe is the handler above, which marks the conversation unresumable on disk:
|
|
366
|
+
// nothing will ever walk this chain again. Returning a config that names a checkpoint we did
|
|
367
|
+
// not write is the lesser evil against throwing, which ends the user's session.
|
|
368
|
+
return {
|
|
369
|
+
configurable: {
|
|
370
|
+
thread_id: threadId,
|
|
371
|
+
checkpoint_ns: checkpointNs,
|
|
372
|
+
checkpoint_id: checkpoint.id,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async putWrites(config, writes, taskId) {
|
|
377
|
+
const threadId = stringField(config, 'thread_id');
|
|
378
|
+
if (threadId === undefined) {
|
|
379
|
+
throw new Error('Failed to put writes: the RunnableConfig is missing a "thread_id" in its ' +
|
|
380
|
+
'"configurable" property.');
|
|
381
|
+
}
|
|
382
|
+
const checkpointId = checkpointIdOf(config);
|
|
383
|
+
if (!checkpointId) {
|
|
384
|
+
throw new Error('Failed to put writes: the RunnableConfig is missing a "checkpoint_id" in its ' +
|
|
385
|
+
'"configurable" property.');
|
|
386
|
+
}
|
|
387
|
+
this.writtenThreads.add(threadId);
|
|
388
|
+
const checkpointNs = stringField(config, 'checkpoint_ns') ?? '';
|
|
389
|
+
try {
|
|
390
|
+
// Two statements, chosen per write by the sign of its slot, because the two halves of the
|
|
391
|
+
// contract differ: an ordinary write (idx >= 0) is insert-ONCE, so a retried super-step
|
|
392
|
+
// writing the same slot again must leave the first value alone, while a reserved channel
|
|
393
|
+
// ({@link WRITES_IDX_MAP}) is replace-on-write, so the newest error / interrupt / resume wins.
|
|
394
|
+
// One blanket `INSERT OR REPLACE` would get the ordinary case wrong and one blanket
|
|
395
|
+
// `INSERT OR IGNORE` the reserved one, and either divergence is visible only on a resume.
|
|
396
|
+
const insertOnce = this.db.prepare(`INSERT OR IGNORE INTO checkpoint_writes
|
|
397
|
+
(thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value)
|
|
398
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
399
|
+
const replace = this.db.prepare(`INSERT OR REPLACE INTO checkpoint_writes
|
|
400
|
+
(thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value)
|
|
401
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
402
|
+
// **Not wrapped in a transaction, deliberately.** A failure partway through this loop leaves
|
|
403
|
+
// the earlier writes of the same task committed, which LangGraph tolerates: `putWrites` is
|
|
404
|
+
// insert-once per (task, idx), so the retry that follows re-writes the same slots and the
|
|
405
|
+
// survivors are ignored rather than duplicated. A transaction would buy atomicity across the
|
|
406
|
+
// task's slots at the cost of holding a write lock across `dumpsTyped` — serialization, inside
|
|
407
|
+
// the lock, on a file the history recorder also opens every turn. That is the trade to revisit
|
|
408
|
+
// if a reserved-channel write ever has to land atomically with an ordinary one; today none
|
|
409
|
+
// does. Under the degrade posture a torn task is doubly harmless: the conversation is marked
|
|
410
|
+
// unresumable, so nothing will read these rows again.
|
|
411
|
+
for (let position = 0; position < writes.length; position++) {
|
|
412
|
+
const [channel, value] = writes[position];
|
|
413
|
+
// `Object.hasOwn`, not a plain index, because a channel name is arbitrary text arriving
|
|
414
|
+
// from the graph: `WRITES_IDX_MAP['constructor']` resolves up the PROTOTYPE CHAIN to
|
|
415
|
+
// `Object`, and `toString` / `valueOf` likewise, so a plain read would hand `idx` a function
|
|
416
|
+
// and take the reserved-vs-positional branch on it. `Object.freeze` does not help — it seals
|
|
417
|
+
// the own properties and leaves the prototype reachable. Upstream's `WRITES_IDX_MAP` has the
|
|
418
|
+
// same shape; that is a reason to keep the mirror, not to keep the defect.
|
|
419
|
+
const reserved = Object.hasOwn(WRITES_IDX_MAP, channel)
|
|
420
|
+
? WRITES_IDX_MAP[channel]
|
|
421
|
+
: undefined;
|
|
422
|
+
const idx = reserved ?? position;
|
|
423
|
+
const [type, serialized] = await this.serde.dumpsTyped(value);
|
|
424
|
+
const statement = idx < 0 ? replace : insertOnce;
|
|
425
|
+
statement.run(threadId, checkpointNs, checkpointId, taskId, idx, channel, type, serialized);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
this.reportWriteFailure(error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async deleteThread(threadId) {
|
|
433
|
+
deleteThreads(this.db, [threadId]);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* GS2-107 — delete every thread no conversation row names, past the grace window. The retention
|
|
437
|
+
* module owns the policy and the reasoning; this is the saver's own connection lent to it, so a
|
|
438
|
+
* session that already has the store open can reclaim without opening it again.
|
|
439
|
+
*
|
|
440
|
+
* **Every thread this saver wrote is excluded, always**, on top of whatever the caller names. A
|
|
441
|
+
* caller cannot supply that set: the runner rotates threads without telling anyone, so the only
|
|
442
|
+
* place that knows which thread the session ended on is the object the writes went through. The
|
|
443
|
+
* union is the contract — `excludeThreadIds` adds to the protection and can never subtract from
|
|
444
|
+
* it.
|
|
445
|
+
*/
|
|
446
|
+
reclaimUnresumableThreads(options = {}) {
|
|
447
|
+
return reclaimUnresumableThreads(this.db, {
|
|
448
|
+
...options,
|
|
449
|
+
excludeThreadIds: [...this.writtenThreads, ...(options.excludeThreadIds ?? [])],
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/** GS2-107 — what the checkpoint tables hold, over this saver's open connection. */
|
|
453
|
+
storeStats(dbPath, topN) {
|
|
454
|
+
return collectCheckpointStoreStats(this.db, dbPath, topN);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Fail-soft open of the durable checkpoint saver. Returns `null` (never throws) when the DB cannot
|
|
459
|
+
* be opened or its tables cannot be created.
|
|
460
|
+
*/
|
|
461
|
+
export function openCheckpointSaver(dbPath, options = {}) {
|
|
462
|
+
return GthSqliteSaver.open(dbPath, options);
|
|
463
|
+
}
|
|
464
|
+
//# sourceMappingURL=checkpointSaver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpointSaver.js","sourceRoot":"","sources":["../../src/history/checkpointSaver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-7 (B20) / GS2-20 — the ONE switch governing everything the local history feature persists:
|
|
4
|
+
* the per-turn session records and the durable LangGraph checkpoints a resume reads back.
|
|
5
|
+
*
|
|
6
|
+
* They share a switch rather than getting one each because they are two halves of one promise. A
|
|
7
|
+
* conversation listed by `gth history list` that cannot be resumed, or a resumable thread with no
|
|
8
|
+
* listing to find it from, is a state a user has no way to reason about — and a second key would
|
|
9
|
+
* make both reachable.
|
|
10
|
+
*/
|
|
11
|
+
/** The subset of the resolved config history reads (structural, to avoid a hard type dep). */
|
|
12
|
+
export interface HistoryConfigView {
|
|
13
|
+
history?: {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
dbPath?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether this run persists history. **Absent means ON**: a default run records its turns to the
|
|
20
|
+
* local store and checkpoints its graph state, both under the user's own `~/.gsloth` dir and
|
|
21
|
+
* neither leaving the machine.
|
|
22
|
+
*
|
|
23
|
+
* `history.enabled: false` is the opt-out and is the only value that turns it off, which is why
|
|
24
|
+
* this tests against `false` rather than for truthiness — the difference between "absent" and
|
|
25
|
+
* "explicitly off" is the whole behaviour.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isHistoryEnabled(config: HistoryConfigView | undefined): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-7 (B20) / GS2-20 — the ONE switch governing everything the local history feature persists:
|
|
4
|
+
* the per-turn session records and the durable LangGraph checkpoints a resume reads back.
|
|
5
|
+
*
|
|
6
|
+
* They share a switch rather than getting one each because they are two halves of one promise. A
|
|
7
|
+
* conversation listed by `gth history list` that cannot be resumed, or a resumable thread with no
|
|
8
|
+
* listing to find it from, is a state a user has no way to reason about — and a second key would
|
|
9
|
+
* make both reachable.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Whether this run persists history. **Absent means ON**: a default run records its turns to the
|
|
13
|
+
* local store and checkpoints its graph state, both under the user's own `~/.gsloth` dir and
|
|
14
|
+
* neither leaving the machine.
|
|
15
|
+
*
|
|
16
|
+
* `history.enabled: false` is the opt-out and is the only value that turns it off, which is why
|
|
17
|
+
* this tests against `false` rather than for truthiness — the difference between "absent" and
|
|
18
|
+
* "explicitly off" is the whole behaviour.
|
|
19
|
+
*/
|
|
20
|
+
export function isHistoryEnabled(config) {
|
|
21
|
+
return config?.history?.enabled !== false;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=historyEnabled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"historyEnabled.js","sourceRoot":"","sources":["../../src/history/historyEnabled.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAqC;IACpE,OAAO,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC;AAC5C,CAAC"}
|