@botbotgo/agent-harness 0.0.284 → 0.0.285

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 CHANGED
@@ -859,8 +859,8 @@ spec:
859
859
  path: store.sqlite
860
860
  - kind: Checkpointer
861
861
  name: default
862
- checkpointerKind: FileCheckpointer
863
- path: checkpoints.json
862
+ checkpointerKind: SqliteSaver
863
+ path: runtime/checkpoints.sqlite
864
864
  ```
865
865
 
866
866
  ### `config/runtime/runtime-memory.yaml`
package/README.zh.md CHANGED
@@ -818,8 +818,8 @@ spec:
818
818
  path: store.sqlite
819
819
  - kind: Checkpointer
820
820
  name: default
821
- checkpointerKind: FileCheckpointer
822
- path: checkpoints.json
821
+ checkpointerKind: SqliteSaver
822
+ path: runtime/checkpoints.sqlite
823
823
  ```
824
824
 
825
825
  ### `config/runtime/runtime-memory.yaml`
@@ -33,8 +33,8 @@ spec:
33
33
  mcpServers: []
34
34
  # Runtime execution feature: checkpointer config passed into the selected backend adapter.
35
35
  # Even the lightweight direct path can benefit from resumable state during interactive use.
36
- # Available `kind` options in this harness: `FileCheckpointer`, `MemorySaver`.
37
- # The repository default uses the file-backed preset so durable checkpoint state does not require native sqlite bindings.
36
+ # Available `kind` options in this harness: `SqliteSaver`, `FileCheckpointer`, `MemorySaver`.
37
+ # The repository default uses the sqlite-backed preset so durable checkpoint state stays inside `runtime/checkpoints.sqlite`.
38
38
  checkpointer: default
39
39
  # Upstream execution feature: LangGraph store available to middleware and runtime context hooks.
40
40
  # The default direct host keeps this enabled so middleware can use the same durable store surface as other hosts.
@@ -40,8 +40,8 @@ spec:
40
40
  mcpServers: []
41
41
  # Runtime execution feature: checkpointer config passed into the selected backend adapter.
42
42
  # This persists resumable graph state for this agent.
43
- # Available `kind` options in this harness: `FileCheckpointer`, `MemorySaver`.
44
- # The repository default uses the file-backed preset so durable checkpoint state does not require native sqlite bindings.
43
+ # Available `kind` options in this harness: `SqliteSaver`, `FileCheckpointer`, `MemorySaver`.
44
+ # The repository default uses the sqlite-backed preset so durable checkpoint state stays inside `runtime/checkpoints.sqlite`.
45
45
  checkpointer: default
46
46
  # Upstream execution feature: store config passed into the selected backend adapter.
47
47
  # In the default deepagent adapter this is the LangGraph store used by `StoreBackend` routes.
@@ -13,6 +13,6 @@ spec:
13
13
  # agent-harness feature: reusable checkpointer preset for resumable execution state.
14
14
  - kind: Checkpointer
15
15
  name: default
16
- description: Default file-backed checkpointer preset for durable local graph state without native sqlite bindings.
17
- checkpointerKind: FileCheckpointer
18
- path: runtime/checkpoints.json
16
+ description: Default sqlite-backed checkpointer preset for durable local graph state under the runtime data root.
17
+ checkpointerKind: SqliteSaver
18
+ path: runtime/checkpoints.sqlite
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.283";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.284";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.283";
1
+ export const AGENT_HARNESS_VERSION = "0.0.284";
@@ -43,13 +43,13 @@ export async function resolveVectorStore(workspace, vectorStores, vectorStoreRef
43
43
  return resolved;
44
44
  }
45
45
  export function resolveCheckpointer(checkpointers, binding) {
46
- const key = `${binding.harnessRuntime.runRoot}:${JSON.stringify(binding.harnessRuntime.checkpointer ?? { kind: "FileCheckpointer", path: resolveRuntimeCheckpointerPath(binding.harnessRuntime.runRoot, "checkpoints.json") })}`;
46
+ const key = `${binding.harnessRuntime.runRoot}:${JSON.stringify(binding.harnessRuntime.checkpointer ?? { kind: "SqliteSaver", path: resolveRuntimeCheckpointerPath(binding.harnessRuntime.runRoot, "checkpoints.sqlite") })}`;
47
47
  const existing = checkpointers.get(key);
48
48
  if (existing) {
49
49
  return existing;
50
50
  }
51
51
  const resolvedConfig = binding.harnessRuntime.checkpointer ??
52
- { kind: "FileCheckpointer", path: resolveRuntimeCheckpointerPath(binding.harnessRuntime.runRoot, "checkpoints.json") };
52
+ { kind: "SqliteSaver", path: resolveRuntimeCheckpointerPath(binding.harnessRuntime.runRoot, "checkpoints.sqlite") };
53
53
  if (typeof resolvedConfig === "boolean") {
54
54
  checkpointers.set(key, resolvedConfig);
55
55
  return resolvedConfig;
@@ -1,4 +1,6 @@
1
1
  import path from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import { createClient } from "@libsql/client";
2
4
  import { fileExists } from "../../utils/fs.js";
3
5
  import { resolveRuntimeCheckpointerPath } from "../support/runtime-layout.js";
4
6
  import { getRuntimeDefaults } from "../../workspace/support/workspace-ref-utils.js";
@@ -73,13 +75,49 @@ export function discoverCheckpointMaintenanceTargets(workspace) {
73
75
  export function maintainSqliteCheckpoints(dbPath, config, nowMs = Date.now()) {
74
76
  return maintainSqliteCheckpointsInternal(dbPath, config, nowMs);
75
77
  }
76
- async function maintainSqliteCheckpointsInternal(dbPath, config, _nowMs) {
78
+ async function maintainSqliteCheckpointsInternal(dbPath, config, nowMs) {
77
79
  if (!(await fileExists(dbPath))) {
78
80
  return { deletedCount: 0 };
79
81
  }
80
- void config;
82
+ const client = createClient({ url: `file:${dbPath}` });
83
+ await client.execute("PRAGMA journal_mode=WAL");
84
+ await client.execute("PRAGMA foreign_keys=ON");
85
+ await client.execute("PRAGMA busy_timeout=5000");
86
+ await client.execute(`
87
+ CREATE TABLE IF NOT EXISTS checkpoint_state (
88
+ state_key TEXT PRIMARY KEY,
89
+ state_json TEXT NOT NULL,
90
+ updated_at TEXT NOT NULL
91
+ )
92
+ `);
93
+ const result = await client.execute("SELECT state_key, updated_at FROM checkpoint_state");
94
+ if (result.rows.length === 0) {
95
+ return { deletedCount: 0 };
96
+ }
97
+ const stats = await stat(dbPath).catch(() => null);
98
+ const maxAgeMs = config.policies.maxAgeSeconds ? config.policies.maxAgeSeconds * 1000 : null;
99
+ const maxBytes = config.policies.maxBytes ?? null;
100
+ const sweepBatchSize = Math.max(1, config.sqlite.sweepBatchSize);
101
+ let deletedCount = 0;
102
+ for (const row of result.rows.slice(0, sweepBatchSize)) {
103
+ const stateKey = typeof row.state_key === "string" ? row.state_key : "";
104
+ const updatedAtMs = typeof row.updated_at === "string" ? Date.parse(row.updated_at) : NaN;
105
+ const exceededAge = maxAgeMs !== null && Number.isFinite(updatedAtMs) && nowMs - updatedAtMs > maxAgeMs;
106
+ const exceededBytes = maxBytes !== null && stats !== null && stats.size > maxBytes;
107
+ if (!exceededAge && !exceededBytes) {
108
+ continue;
109
+ }
110
+ await client.execute({
111
+ sql: "DELETE FROM checkpoint_state WHERE state_key = ?",
112
+ args: [stateKey],
113
+ });
114
+ deletedCount += 1;
115
+ }
116
+ if (deletedCount > 0 && config.sqlite.vacuum) {
117
+ await client.execute("VACUUM");
118
+ }
81
119
  void path.dirname(dbPath);
82
- throw new Error("Checkpoint maintenance for SqliteSaver is not supported in this runtime right now");
120
+ return { deletedCount };
83
121
  }
84
122
  export class CheckpointMaintenanceLoop {
85
123
  targets;
@@ -1,3 +1,4 @@
1
1
  export * from "./checkpoint-maintenance.js";
2
2
  export * from "./file-checkpoint-saver.js";
3
+ export * from "./sqlite-checkpoint-saver.js";
3
4
  export * from "./runtime-record-maintenance.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./checkpoint-maintenance.js";
2
2
  export * from "./file-checkpoint-saver.js";
3
+ export * from "./sqlite-checkpoint-saver.js";
3
4
  export * from "./runtime-record-maintenance.js";
@@ -0,0 +1,28 @@
1
+ import { MemorySaver } from "@langchain/langgraph";
2
+ type MemorySaverConfig = Parameters<MemorySaver["getTuple"]>[0];
3
+ type MemorySaverListOptions = Parameters<MemorySaver["list"]>[1];
4
+ type MemorySaverPutCheckpoint = Parameters<MemorySaver["put"]>[1];
5
+ type MemorySaverPutMetadata = Parameters<MemorySaver["put"]>[2];
6
+ type MemorySaverPutWrites = Parameters<MemorySaver["putWrites"]>[1];
7
+ type MemorySaverPutResult = ReturnType<MemorySaver["put"]>;
8
+ export declare class SqliteCheckpointSaver extends MemorySaver {
9
+ readonly filePath: string;
10
+ private client;
11
+ private loaded;
12
+ private initialized;
13
+ private initialization;
14
+ private operationChain;
15
+ constructor(filePath: string);
16
+ private runSerialized;
17
+ private getClient;
18
+ private ensureInitialized;
19
+ private loadStateRow;
20
+ private ensureLoaded;
21
+ private persist;
22
+ getTuple(config: MemorySaverConfig): Promise<import("@langchain/langgraph").CheckpointTuple | undefined>;
23
+ list(config: MemorySaverConfig, options?: MemorySaverListOptions): AsyncGenerator<import("@langchain/langgraph").CheckpointTuple, void, unknown>;
24
+ put(config: MemorySaverConfig, checkpoint: MemorySaverPutCheckpoint, metadata: MemorySaverPutMetadata): MemorySaverPutResult;
25
+ putWrites(config: MemorySaverConfig, writes: MemorySaverPutWrites, taskId: string): Promise<void>;
26
+ deleteThread(threadId: string): Promise<void>;
27
+ }
28
+ export { SqliteCheckpointSaver as SqliteSaver };
@@ -0,0 +1,177 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { MemorySaver } from "@langchain/langgraph";
4
+ import { createClient } from "@libsql/client";
5
+ const CHECKPOINT_TABLE = "checkpoint_state";
6
+ const DEFAULT_STATE_KEY = "default";
7
+ function encodeBinary(value) {
8
+ if (value instanceof Uint8Array) {
9
+ return {
10
+ __type: "Uint8Array",
11
+ data: Array.from(value),
12
+ };
13
+ }
14
+ if (Array.isArray(value)) {
15
+ return value.map((item) => encodeBinary(item));
16
+ }
17
+ if (typeof value === "object" && value) {
18
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, encodeBinary(entry)]));
19
+ }
20
+ return value;
21
+ }
22
+ function decodeBinary(value) {
23
+ if (Array.isArray(value)) {
24
+ return value.map((item) => decodeBinary(item));
25
+ }
26
+ if (typeof value === "object" && value) {
27
+ const typed = value;
28
+ if (typed.__type === "Uint8Array" && Array.isArray(typed.data)) {
29
+ return new Uint8Array(typed.data.map((item) => Number(item)));
30
+ }
31
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeBinary(entry)]));
32
+ }
33
+ return value;
34
+ }
35
+ function pruneThreadEntries(record, threadId) {
36
+ for (const key of Object.keys(record)) {
37
+ if (key.includes(threadId)) {
38
+ delete record[key];
39
+ continue;
40
+ }
41
+ const value = record[key];
42
+ if (typeof value === "object" && value && !Array.isArray(value)) {
43
+ pruneThreadEntries(value, threadId);
44
+ if (Object.keys(value).length === 0) {
45
+ delete record[key];
46
+ }
47
+ }
48
+ }
49
+ }
50
+ export class SqliteCheckpointSaver extends MemorySaver {
51
+ filePath;
52
+ client = null;
53
+ loaded = false;
54
+ initialized = false;
55
+ initialization = null;
56
+ operationChain = Promise.resolve();
57
+ constructor(filePath) {
58
+ super();
59
+ this.filePath = filePath;
60
+ mkdirSync(path.dirname(filePath), { recursive: true });
61
+ }
62
+ async runSerialized(operation) {
63
+ const pending = this.operationChain.then(operation, operation);
64
+ this.operationChain = pending.then(() => undefined, () => undefined);
65
+ return pending;
66
+ }
67
+ async getClient() {
68
+ if (!this.client) {
69
+ this.client = createClient({ url: `file:${this.filePath}` });
70
+ }
71
+ return this.client;
72
+ }
73
+ async ensureInitialized() {
74
+ if (this.initialized) {
75
+ return;
76
+ }
77
+ if (this.initialization) {
78
+ await this.initialization;
79
+ return;
80
+ }
81
+ this.initialization = (async () => {
82
+ const client = await this.getClient();
83
+ await client.execute("PRAGMA journal_mode=WAL");
84
+ await client.execute("PRAGMA foreign_keys=ON");
85
+ await client.execute("PRAGMA busy_timeout=5000");
86
+ await client.execute(`
87
+ CREATE TABLE IF NOT EXISTS ${CHECKPOINT_TABLE} (
88
+ state_key TEXT PRIMARY KEY,
89
+ state_json TEXT NOT NULL,
90
+ updated_at TEXT NOT NULL
91
+ )
92
+ `);
93
+ this.initialized = true;
94
+ this.initialization = null;
95
+ })();
96
+ await this.initialization;
97
+ }
98
+ async loadStateRow() {
99
+ await this.ensureInitialized();
100
+ const client = await this.getClient();
101
+ const result = await client.execute({
102
+ sql: `SELECT state_json, updated_at FROM ${CHECKPOINT_TABLE} WHERE state_key = ?`,
103
+ args: [DEFAULT_STATE_KEY],
104
+ });
105
+ return result.rows[0] ?? null;
106
+ }
107
+ async ensureLoaded() {
108
+ if (this.loaded) {
109
+ return;
110
+ }
111
+ const row = await this.loadStateRow();
112
+ if (row) {
113
+ const parsed = JSON.parse(row.state_json);
114
+ this.storage = decodeBinary(parsed.storage ?? {});
115
+ this.writes = decodeBinary(parsed.writes ?? {});
116
+ }
117
+ else {
118
+ this.storage = {};
119
+ this.writes = {};
120
+ }
121
+ this.loaded = true;
122
+ }
123
+ async persist() {
124
+ await this.ensureInitialized();
125
+ const client = await this.getClient();
126
+ const now = new Date().toISOString();
127
+ const payload = JSON.stringify({
128
+ storage: this.storage,
129
+ writes: this.writes,
130
+ }, (_key, value) => encodeBinary(value));
131
+ await client.execute({
132
+ sql: `INSERT INTO ${CHECKPOINT_TABLE} (state_key, state_json, updated_at)
133
+ VALUES (?, ?, ?)
134
+ ON CONFLICT(state_key) DO UPDATE SET
135
+ state_json = excluded.state_json,
136
+ updated_at = excluded.updated_at`,
137
+ args: [DEFAULT_STATE_KEY, payload, now],
138
+ });
139
+ }
140
+ async getTuple(config) {
141
+ return this.runSerialized(async () => {
142
+ await this.ensureLoaded();
143
+ return super.getTuple(config);
144
+ });
145
+ }
146
+ async *list(config, options) {
147
+ await this.ensureLoaded();
148
+ for await (const item of super.list(config, options)) {
149
+ yield item;
150
+ }
151
+ }
152
+ async put(config, checkpoint, metadata) {
153
+ return this.runSerialized(async () => {
154
+ await this.ensureLoaded();
155
+ const result = await super.put(config, checkpoint, metadata);
156
+ await this.persist();
157
+ return result;
158
+ });
159
+ }
160
+ async putWrites(config, writes, taskId) {
161
+ return this.runSerialized(async () => {
162
+ await this.ensureLoaded();
163
+ const result = await super.putWrites(config, writes, taskId);
164
+ await this.persist();
165
+ return result;
166
+ });
167
+ }
168
+ async deleteThread(threadId) {
169
+ return this.runSerialized(async () => {
170
+ await this.ensureLoaded();
171
+ pruneThreadEntries(this.storage, threadId);
172
+ pruneThreadEntries(this.writes, threadId);
173
+ await this.persist();
174
+ });
175
+ }
176
+ }
177
+ export { SqliteCheckpointSaver as SqliteSaver };
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { MemorySaver } from "@langchain/langgraph";
3
3
  import { FileCheckpointSaver } from "../maintenance/file-checkpoint-saver.js";
4
+ import { SqliteCheckpointSaver } from "../maintenance/sqlite-checkpoint-saver.js";
4
5
  import { createInMemoryStore, FileBackedStore, SqliteBackedStore } from "../harness/system/store.js";
5
6
  import { resolveKnowledgeFileStorePath, resolveKnowledgeStorePath, resolveRuntimeCheckpointerPath, } from "./runtime-layout.js";
6
7
  export function createStoreForConfig(storeConfig, runRoot) {
@@ -28,12 +29,16 @@ export function createCheckpointerForConfig(checkpointerConfig, runRoot) {
28
29
  if (typeof checkpointerConfig === "boolean") {
29
30
  return checkpointerConfig;
30
31
  }
31
- const kind = typeof checkpointerConfig.kind === "string" ? checkpointerConfig.kind : "FileCheckpointer";
32
+ const kind = typeof checkpointerConfig.kind === "string" ? checkpointerConfig.kind : "SqliteSaver";
32
33
  switch (kind) {
33
34
  case "MemorySaver":
34
35
  return new MemorySaver();
35
- case "SqliteSaver":
36
- throw new Error("Checkpointer kind SqliteSaver is not supported in this runtime right now");
36
+ case "SqliteSaver": {
37
+ const configuredPath = typeof checkpointerConfig.path === "string"
38
+ ? String(checkpointerConfig.path)
39
+ : resolveRuntimeCheckpointerPath(runRoot, "checkpoints.sqlite");
40
+ return new SqliteCheckpointSaver(path.isAbsolute(configuredPath) ? configuredPath : path.join(runRoot, configuredPath));
41
+ }
37
42
  case "FileCheckpointer": {
38
43
  const configuredPath = typeof checkpointerConfig.path === "string"
39
44
  ? String(checkpointerConfig.path)
@@ -10,7 +10,7 @@ function validateCheckpointerConfig(agent) {
10
10
  return;
11
11
  }
12
12
  const typedCheckpointer = checkpointer;
13
- const kind = typeof typedCheckpointer.kind === "string" ? typedCheckpointer.kind : "FileCheckpointer";
13
+ const kind = typeof typedCheckpointer.kind === "string" ? typedCheckpointer.kind : "SqliteSaver";
14
14
  const hasPath = typeof typedCheckpointer.path === "string" && typedCheckpointer.path.trim().length > 0;
15
15
  if (kind === "MemorySaver" && hasPath) {
16
16
  throw new Error(`Agent ${agent.id} checkpointer.path is not supported for kind MemorySaver`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.284",
3
+ "version": "0.0.285",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",