@kortyx/runtime 0.18.0 → 0.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.0](https://github.com/kortyx-io/kortyx/compare/runtime-v0.18.0...runtime-v0.19.0) (2026-09-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * **runtime:** add PostgreSQL persistence, caching composition, and retention ([#207](https://github.com/kortyx-io/kortyx/issues/207)) ([6c1e7df](https://github.com/kortyx-io/kortyx/commit/6c1e7df22ffe8c792bf52b7dba5ae6ea04c1eb2a))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @kortyx/hooks bumped to 0.25.3
16
+
3
17
  ## [0.18.0](https://github.com/kortyx-io/kortyx/compare/runtime-v0.17.2...runtime-v0.18.0) (2026-09-16)
4
18
 
5
19
 
package/README.md CHANGED
@@ -27,6 +27,7 @@ npm install @kortyx/runtime
27
27
  - `getRegisteredNode(...)`
28
28
  - `createInMemoryFrameworkAdapter(...)`
29
29
  - `createRedisFrameworkAdapter(...)`
30
+ - `createPostgresFrameworkAdapter(...)`
30
31
  - `createFrameworkAdapterFromEnv(...)`
31
32
 
32
33
  ## Persistence
@@ -51,3 +52,53 @@ export const framework = createRedisFrameworkAdapter({
51
52
  ## License
52
53
 
53
54
  Apache-2.0. See [LICENSE](https://github.com/kortyx-io/kortyx/blob/main/LICENSE).
55
+
56
+ ## PostgreSQL and retention
57
+
58
+ Use `createPostgresFrameworkAdapter` for durable runtime history. PostgreSQL is authoritative. Optionally attach a Redis adapter as a checkpoint payload cache with `createCachingFrameworkAdapter({ storage, cache })`.
59
+
60
+ ```ts
61
+ import {
62
+ createPostgresFrameworkAdapter,
63
+ createRedisFrameworkAdapter,
64
+ createCachingFrameworkAdapter,
65
+ } from "kortyx";
66
+
67
+ const storage = createPostgresFrameworkAdapter({
68
+ connectionString: process.env.KORTYX_POSTGRES_URL!,
69
+ namespace: "my-app",
70
+ ttlMs: 7 * 24 * 60 * 60 * 1000, // Approval lifetime, independent of history.
71
+ retention: { checkpointHistoryDays: 30, inactiveSessionDays: 30 },
72
+ });
73
+
74
+ // Optional Redis payload caching; omit the helper to use storage alone.
75
+ const cache = createRedisFrameworkAdapter({
76
+ url: process.env.REDIS_URL!,
77
+ ttlMs: 15 * 60 * 1000,
78
+ });
79
+ const persistence = createCachingFrameworkAdapter({ storage, cache, timeoutMs: 25 });
80
+
81
+ // Run during deployment before serving requests.
82
+ await persistence.maintenance.setup();
83
+
84
+ // Pass persistence as createAgent({ workflows, frameworkAdapter: persistence }).
85
+ // Your scheduled job/worker performs storage maintenance:
86
+ const result = await persistence.maintenance.prune({ batchSize: 500 });
87
+
88
+ // After in-flight executions finish, close owned connections on shutdown.
89
+ await persistence.close();
90
+ ```
91
+
92
+ History and inactive sessions default to 30 days, without a 50-checkpoint cap. Interrupts default to 15 minutes; set `ttlMs` explicitly for longer pauses. Current session heads, unexpired pauses, and executing runs are protected. Rollback preserves abandoned branches until retention expires. Reads enforce expiry even before cleanup runs. The app owns scheduling; Kortyx owns safe, bounded pruning.
93
+
94
+ `KORTYX_POSTGRES_URL` takes precedence over Redis during env-based selection, with Redis used as a cache when configured. `DATABASE_URL` is not used implicitly. Call `maintenance.setup()` before traffic for every built-in adapter; PostgreSQL creates its schema, while Redis and memory need no schema. `maintenance.prune()` and `close()` are also available on every built-in adapter.
95
+
96
+ PostgreSQL setup applies the SDK's numbered SQL migrations in order, using the existing PostgreSQL client. Each migration commits its changes and version together under a schema lock; reruns skip applied versions. Failed migrations roll back and stop setup, while earlier successful migrations remain committed. Retries resume at the first unapplied version. Newer schemas and gaps in recorded history are rejected. Migrations run only through explicit setup, never in request handlers. Released migrations are immutable; future schema changes add a consecutive migration. Applied SQL is verified against a stored SHA-256 checksum; editing it causes setup to reject. Migration advisory and DDL lock waits are capped at five seconds, preserving stricter database settings; a timeout rolls back that migration and requires retry. This bounds lock acquisition, not migration execution time. The original version-only v1 ledger adopts the current v1 checksum once, without rerunning schema SQL; its prior contents cannot be verified retroactively. Use compatible additive changes during rolling deployments; no automatic down migrations or workflow-state conversions are provided.
97
+
98
+ Existing factories remain supported. Their shared `ManagedFrameworkAdapter` lifecycle contract requires implementations for every built-in backend; existing custom `FrameworkAdapter` implementations remain compatible. Redis prune reports zero explicit deletions because native TTL owns expiry. Memory prune removes expired approvals in bounded batches; its session history remains count-limited.
99
+
100
+ Configure the caching helper before traffic. It returns the exact storage adapter and preserves all current/future methods. Currently PostgreSQL storage with Redis caching is supported; unsupported combinations, repeated bindings, and sharing an already-owned cache are rejected. Cache `ttlMs` comes from the Redis adapter; approval `ttlMs` comes from PostgreSQL. The returned adapter owns both connections: close it after executions finish and do not independently use its cache input as another storage backend. Cache keys are separate from standalone Redis runtime keys. Data transfer between storage backends is outside this contract.
101
+
102
+ Run real-service integration coverage with `KORTYX_TEST_POSTGRES_URL=... KORTYX_TEST_REDIS_URL=... pnpm --filter @kortyx/runtime test:integration:postgres`. Use a disposable test database. Redis is optional for these tests.
103
+
104
+ Graph checkpoints are saved asynchronously while tokens stream; cache fills do not block authoritative reads. Cache operations default to a 25 ms budget, and cache failures bypass Redis for five seconds. Required PostgreSQL reads and durable acknowledgements still add latency. After building the SDK, compare persistence overhead using an immediate mock model with `KORTYX_TEST_POSTGRES_URL=... KORTYX_TEST_REDIS_URL=... node scripts/benchmark-runtime-persistence.cjs` from the repository root. The benchmark reports generation start, first token, and execution completion at p50/p95; use disposable services and measure production network conditions separately.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,38 @@
1
- import { BaseCheckpointSaver, CheckpointTuple } from '@langchain/langgraph-checkpoint';
1
+ import { BaseCheckpointSaver, CheckpointTuple, CheckpointListOptions, Checkpoint, CheckpointMetadata, PendingWrite } from '@langchain/langgraph-checkpoint';
2
2
  import * as _kortyx_core from '@kortyx/core';
3
3
  import { GraphState, WorkflowDefinition, NodeFn, RuntimeEnvelope } from '@kortyx/core';
4
+ import { RunnableConfig } from '@langchain/core/runnables';
5
+ import postgres from 'postgres';
4
6
  import * as _kortyx_hooks from '@kortyx/hooks';
5
7
  import { ReasonTraceAdapter, KortyxTelemetryConfig } from '@kortyx/hooks';
6
8
  import { GetProviderFn } from '@kortyx/providers';
7
9
 
8
10
  declare function getCheckpointer(key: string): BaseCheckpointSaver;
9
11
 
12
+ type PruneRuntimeOptions = {
13
+ /** Defaults to wall-clock time; future timestamps are rejected. */
14
+ now?: Date;
15
+ /** Maximum parent records removed per table. Default: 500, maximum: 1000. */
16
+ batchSize?: number;
17
+ };
18
+ type PruneRuntimeResult = {
19
+ skipped: boolean;
20
+ hasMore: boolean;
21
+ deleted: {
22
+ pendingRequests: number;
23
+ sessionCheckpoints: number;
24
+ graphCheckpoints: number;
25
+ sessions: number;
26
+ runs: number;
27
+ };
28
+ };
29
+ type RuntimeMaintenance = {
30
+ /** Explicit, idempotent backend setup. Complete before serving requests. */
31
+ setup: () => Promise<void>;
32
+ /** App-scheduled cleanup. Redis uses native TTL; memory uses lazy expiry/count limits. */
33
+ prune: (options?: PruneRuntimeOptions) => Promise<PruneRuntimeResult>;
34
+ };
35
+
10
36
  /** A self-contained, JSON snapshot, including pending writes needed for replay. */
11
37
  type GraphSnapshotBundle = CheckpointTuple;
12
38
  declare function captureGraphSnapshot(saver: BaseCheckpointSaver, threadId: string, checkpointId?: string): Promise<GraphSnapshotBundle | undefined>;
@@ -60,7 +86,108 @@ interface PendingRequestStore {
60
86
  delete: (token: string) => Promise<void>;
61
87
  update: (token: string, patch: Partial<PendingRequestRecord>) => Promise<void>;
62
88
  }
63
- declare function createInMemoryPendingRequestStore(): PendingRequestStore;
89
+ declare function createInMemoryPendingRequestStore(): PendingRequestStore & {
90
+ pruneExpired: (now: number, batchSize: number) => number;
91
+ };
92
+
93
+ /** Internal cache capability. It is separate from authoritative framework stores. */
94
+ type FrameworkPayloadCache = {
95
+ get: (key: string) => Promise<string | null>;
96
+ set: (key: string, value: string, ttlMs: number) => Promise<void>;
97
+ close?: () => Promise<void>;
98
+ };
99
+ type CreateCachingFrameworkAdapterOptions<T extends ManagedFrameworkAdapter = ManagedFrameworkAdapter> = {
100
+ /** Authoritative storage. Currently supports the PostgreSQL adapter. */
101
+ storage: T;
102
+ /** Payload cache provider. Currently supports the Redis adapter. */
103
+ cache: FrameworkAdapter;
104
+ /** Cache operation budget. Default: 25 ms, maximum: 1000 ms. */
105
+ timeoutMs?: number;
106
+ };
107
+ /** Attaches caching to storage before traffic, preserving every storage method and its identity.
108
+ * The returned adapter owns shutdown of both inputs. Do not use the cache input as another storage backend.
109
+ */
110
+ declare function createCachingFrameworkAdapter<T extends ManagedFrameworkAdapter>(options: CreateCachingFrameworkAdapterOptions<T>): T;
111
+
112
+ type RedisFrameworkStore = {
113
+ close?: () => Promise<void>;
114
+ list?: (prefix: string) => Promise<string[]>;
115
+ take?: (key: string) => Promise<string | null>;
116
+ get: (key: string) => Promise<string | null>;
117
+ set: (key: string, value: string, ttlMs: number) => Promise<void>;
118
+ del: (key: string) => Promise<void>;
119
+ hset: (key: string, field: string, value: string) => Promise<void>;
120
+ hsetnx: (key: string, field: string, value: string) => Promise<number>;
121
+ hgetall: (key: string) => Promise<Record<string, string>>;
122
+ expire: (key: string, ttlMs: number) => Promise<void>;
123
+ scanKeys: (prefix: string) => Promise<string[]>;
124
+ delRaw: (keys: string[]) => Promise<void>;
125
+ };
126
+
127
+ type RuntimeSql = postgres.Sql | postgres.TransactionSql;
128
+ type RuntimeRetentionPolicy = {
129
+ /** Rolling history window. The current head and live runs are protected. Default: 30. */
130
+ checkpointHistoryDays?: number;
131
+ /** Inactivity window for a session. Unexpired interrupts and live runs protect it. Default: 30. */
132
+ inactiveSessionDays?: number;
133
+ };
134
+ /** Internal relational storage. Redis never controls visibility or token consumption. */
135
+ declare class PostgresRuntimeStore {
136
+ private cache?;
137
+ private cacheTtlMs;
138
+ private cacheTimeoutMs;
139
+ readonly sql: postgres.Sql;
140
+ readonly scope: string;
141
+ readonly historyMs: number;
142
+ readonly sessionMs: number;
143
+ private readonly cachePrefix;
144
+ private cacheRetryAfter;
145
+ private cacheClosed;
146
+ private readonly cachePopulations;
147
+ constructor(connectionString: string, namespace: string, retention: RuntimeRetentionPolicy, cache?: Pick<RedisFrameworkStore, "get" | "set" | "close">, cacheTtlMs?: number, cacheTimeoutMs?: number);
148
+ get cacheEnabled(): boolean;
149
+ attachCache(cache: FrameworkPayloadCache, ttlMs: number, timeoutMs: number): void;
150
+ setup(): Promise<void>;
151
+ query<T>(query: PromiseLike<T>): Promise<T>;
152
+ transaction<T>(operation: (sql: postgres.TransactionSql) => Promise<T>): Promise<T>;
153
+ liveSession(sql: RuntimeSql, now: number): postgres.PendingQuery<postgres.Row[]>;
154
+ liveCheckpoint(sql: RuntimeSql, now: number): postgres.PendingQuery<postgres.Row[]>;
155
+ liveRun(sql: RuntimeSql, now: number): postgres.PendingQuery<postgres.Row[]>;
156
+ touchRun(sql: postgres.TransactionSql, runId: string): Promise<void>;
157
+ touchSession(sql: postgres.TransactionSql, sessionId: string): Promise<void>;
158
+ payload<T>(key: unknown[], load: () => Promise<T>): Promise<T>;
159
+ private cacheCall;
160
+ close(): Promise<void>;
161
+ }
162
+
163
+ /** Kortyx-owned checkpoint format and relational persistence, independent of LangGraph saver packages. */
164
+ declare class PostgresCheckpointSaver extends BaseCheckpointSaver {
165
+ private readonly store;
166
+ constructor(store: PostgresRuntimeStore);
167
+ getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;
168
+ list(config: RunnableConfig, options?: CheckpointListOptions): AsyncGenerator<CheckpointTuple>;
169
+ put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: Record<string, number | string>): Promise<RunnableConfig>;
170
+ putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
171
+ deleteThread(runId: string): Promise<void>;
172
+ deleteCheckpointWrites(runId: string, ns: string, id: string): Promise<void>;
173
+ getLatestCheckpointId(runId: string, ns?: string): Promise<string | undefined>;
174
+ }
175
+
176
+ type CreatePostgresFrameworkAdapterOptions = {
177
+ connectionString: string;
178
+ /** Isolates applications sharing the runtime tables. Default: "default". */
179
+ namespace?: string;
180
+ /** Interrupt lifetime, independent of checkpoint retention. Default: 15 minutes. */
181
+ ttlMs?: number;
182
+ retention?: RuntimeRetentionPolicy;
183
+ };
184
+ type PostgresFrameworkAdapter = ManagedFrameworkAdapter & {
185
+ kind: "postgres";
186
+ checkpointer: PostgresCheckpointSaver;
187
+ /** Closes owned connections. Call after in-flight executions have completed. */
188
+ close: () => Promise<void>;
189
+ };
190
+ declare function createPostgresFrameworkAdapter(options: CreatePostgresFrameworkAdapterOptions): PostgresFrameworkAdapter;
64
191
 
65
192
  type CheckpointId = string;
66
193
  type CheckpointSummary = {
@@ -76,6 +203,8 @@ type CheckpointSummary = {
76
203
  forkedFrom?: string;
77
204
  workflowVersion?: string;
78
205
  buildId?: string;
206
+ /** PostgreSQL retains abandoned branches until their history window expires. */
207
+ branchStatus?: "active" | "abandoned";
79
208
  };
80
209
  type SessionCheckpointRecord = CheckpointSummary & {
81
210
  runId: string;
@@ -114,13 +243,18 @@ type ForkSessionCheckpointResult = {
114
243
  checkpoint: SessionCheckpointRecord;
115
244
  };
116
245
  type SessionCheckpointStore = {
246
+ /** The store publishes pending requests in the same transaction as the session transition. */
247
+ managesPendingRequests?: boolean;
117
248
  list: (sessionId: string) => Promise<CheckpointSummary[]>;
118
249
  get: (id: CheckpointId) => Promise<SessionCheckpointRecord | null>;
119
250
  getHead: (sessionId: string) => Promise<SessionCheckpointRecord | null>;
120
251
  append: (args: AppendSessionCheckpointArgs) => Promise<SessionCheckpointRecord>;
121
- rollbackTo: (id: CheckpointId) => Promise<RollbackSessionCheckpointResult>;
252
+ rollbackTo: (id: CheckpointId, options?: {
253
+ preparePendingRequests?: (requests: PendingRequestRecord[]) => void;
254
+ }) => Promise<RollbackSessionCheckpointResult>;
122
255
  fork: (id: CheckpointId, options?: {
123
256
  newSessionId?: string;
257
+ preparePendingRequests?: (requests: PendingRequestRecord[]) => void;
124
258
  }) => Promise<ForkSessionCheckpointResult>;
125
259
  };
126
260
  type SessionCheckpointStoreOptions = {
@@ -129,30 +263,50 @@ type SessionCheckpointStoreOptions = {
129
263
  declare function createInMemorySessionCheckpointStore(options?: SessionCheckpointStoreOptions): SessionCheckpointStore;
130
264
 
131
265
  type FrameworkAdapter = {
132
- kind: "in-memory" | "redis";
266
+ kind: "in-memory" | "redis" | "postgres";
133
267
  pendingRequests: PendingRequestStore;
134
268
  sessionCheckpoints: SessionCheckpointStore;
135
269
  checkpointer: BaseCheckpointSaver;
136
270
  ttlMs: number;
271
+ /** Optional for legacy custom adapters; required on every built-in adapter. */
272
+ maintenance?: RuntimeMaintenance;
273
+ /** Optional for legacy custom adapters; closes owned connections. */
274
+ close?: () => Promise<void>;
275
+ /** Protects execution from concurrent retention cleanup. Released after the entire outcome is persisted. */
276
+ acquireRunLease?: (args: {
277
+ runId: string;
278
+ sessionId?: string;
279
+ onLost: (cause: unknown) => void;
280
+ }) => Promise<() => Promise<void>>;
137
281
  /**
138
282
  * Best-effort cleanup for ephemeral framework state for a single run.
139
283
  * Called when a workflow completes without pausing for an interrupt.
140
284
  */
141
285
  cleanupRun?: (runId: string, namespaces: string[]) => Promise<void>;
142
286
  };
287
+ /** Shared lifecycle contract implemented by every built-in adapter.
288
+ * FrameworkAdapter remains available for existing custom implementations.
289
+ */
290
+ type ManagedFrameworkAdapter = FrameworkAdapter & {
291
+ maintenance: RuntimeMaintenance;
292
+ /** Close owned connections after in-flight executions finish. */
293
+ close: () => Promise<void>;
294
+ };
143
295
  type CreateInMemoryFrameworkAdapterOptions = {
144
296
  ttlMs?: number;
145
297
  maxSessionCheckpoints?: number;
146
298
  };
147
- declare function createInMemoryFrameworkAdapter(options?: CreateInMemoryFrameworkAdapterOptions): FrameworkAdapter;
299
+ declare function createInMemoryFrameworkAdapter(options?: CreateInMemoryFrameworkAdapterOptions): ManagedFrameworkAdapter;
148
300
  type CreateRedisFrameworkAdapterOptions = {
149
301
  url: string;
150
302
  ttlMs?: number;
151
303
  prefix?: string;
152
304
  maxSessionCheckpoints?: number;
153
305
  };
154
- declare function createRedisFrameworkAdapter(options: CreateRedisFrameworkAdapterOptions): FrameworkAdapter;
155
- declare function createFrameworkAdapterFromEnv(env?: Record<string, string | undefined>): FrameworkAdapter;
306
+ declare function createRedisFrameworkAdapter(options: CreateRedisFrameworkAdapterOptions): ManagedFrameworkAdapter;
307
+ declare function createFrameworkAdapterFromEnv(env?: Record<string, string | undefined>): (ManagedFrameworkAdapter & {
308
+ kind: "in-memory" | "redis";
309
+ }) | PostgresFrameworkAdapter;
156
310
 
157
311
  interface CompiledGraphBase {
158
312
  invoke(state: unknown, options?: Record<string, unknown>): Promise<unknown>;
@@ -236,4 +390,4 @@ interface InitialStateArgs<Config = unknown> {
236
390
  }
237
391
  declare function buildInitialGraphState<Config>({ input, runtime, config, defaultWorkflowId, }: InitialStateArgs<Config>): Promise<GraphState>;
238
392
 
239
- export { type AppendSessionCheckpointArgs, type CheckpointId, type CheckpointSummary, type CreateInMemoryFrameworkAdapterOptions, type CreateRedisFrameworkAdapterOptions, type ExecutionControl, type ExecutionRuntimeConfig, type FileWorkflowRegistryOptions, type ForkSessionCheckpointResult, type FrameworkAdapter, type GraphSnapshotBundle, type HumanInputKind, type HumanInputOption, type InitialStateArgs, type PendingRequestRecord, type PendingRequestStore, type RollbackSessionCheckpointResult, type SessionCheckpointRecord, type SessionCheckpointStore, type SessionCheckpointStoreOptions, type WorkflowRegistry, buildInitialGraphState, captureGraphSnapshot, clearRegisteredNodes, createExecutionGraph, createFileWorkflowRegistry, createFrameworkAdapterFromEnv, createInMemoryFrameworkAdapter, createInMemoryPendingRequestStore, createInMemorySessionCheckpointStore, createInMemoryWorkflowRegistry, createRedisFrameworkAdapter, getCheckpointer, getRegisteredNode, listRegisteredNodes, makeRequestId, makeResumeToken, registerNode, resolveNode, restoreGraphSnapshot };
393
+ export { type AppendSessionCheckpointArgs, type CheckpointId, type CheckpointSummary, type CreateCachingFrameworkAdapterOptions, type CreateInMemoryFrameworkAdapterOptions, type CreatePostgresFrameworkAdapterOptions, type CreateRedisFrameworkAdapterOptions, type ExecutionControl, type ExecutionRuntimeConfig, type FileWorkflowRegistryOptions, type ForkSessionCheckpointResult, type FrameworkAdapter, type GraphSnapshotBundle, type HumanInputKind, type HumanInputOption, type InitialStateArgs, type ManagedFrameworkAdapter, type PendingRequestRecord, type PendingRequestStore, type PostgresFrameworkAdapter, type PruneRuntimeOptions, type PruneRuntimeResult, type RollbackSessionCheckpointResult, type RuntimeMaintenance, type RuntimeRetentionPolicy, type SessionCheckpointRecord, type SessionCheckpointStore, type SessionCheckpointStoreOptions, type WorkflowRegistry, buildInitialGraphState, captureGraphSnapshot, clearRegisteredNodes, createCachingFrameworkAdapter, createExecutionGraph, createFileWorkflowRegistry, createFrameworkAdapterFromEnv, createInMemoryFrameworkAdapter, createInMemoryPendingRequestStore, createInMemorySessionCheckpointStore, createInMemoryWorkflowRegistry, createPostgresFrameworkAdapter, createRedisFrameworkAdapter, getCheckpointer, getRegisteredNode, listRegisteredNodes, makeRequestId, makeResumeToken, registerNode, resolveNode, restoreGraphSnapshot };