@kortyx/agent 0.23.0 → 0.24.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.24.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.23.0...agent-v0.24.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/runtime bumped to 0.19.0
16
+ * devDependencies
17
+ * @kortyx/hooks bumped to 0.25.3
18
+
3
19
  ## [0.23.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.22.2...agent-v0.23.0) (2026-09-16)
4
20
 
5
21
 
package/dist/index.d.ts CHANGED
@@ -254,6 +254,8 @@ type OrchestrationOutcome = {
254
254
  error?: unknown;
255
255
  };
256
256
  interface OrchestrateArgs {
257
+ /** Resume owns the lease while restoring its snapshot, then transfers it to execution. */
258
+ executionLeaseRelease?: (() => Promise<void>) | undefined;
257
259
  executionSignal?: AbortSignal | undefined;
258
260
  onExecution?: ((completion: Promise<void>) => void) | undefined;
259
261
  abortSignal?: AbortSignal | undefined;
@@ -272,7 +274,7 @@ interface OrchestrateArgs {
272
274
  * Orchestrates runtime execution with mid-stream transitions emitted by
273
275
  * runtime events.
274
276
  */
275
- declare function orchestrateGraphStream({ sessionId, runId, graph, state, config: initialConfig, abortSignal: requestSignal, executionSignal, onExecution, emitOutput, onOutcome, selectWorkflow, knownWorkflowIds, frameworkAdapter, }: OrchestrateArgs): Promise<NodeJS.ReadableStream>;
277
+ declare function orchestrateGraphStream({ sessionId, runId, graph, state, config: initialConfig, abortSignal: requestSignal, executionSignal, onExecution, emitOutput, onOutcome, selectWorkflow, knownWorkflowIds, frameworkAdapter, executionLeaseRelease, }: OrchestrateArgs): Promise<NodeJS.ReadableStream>;
276
278
 
277
279
  interface ResumeMeta {
278
280
  token: string;
package/dist/index.js CHANGED
@@ -3894,7 +3894,8 @@ async function orchestrateGraphStream({
3894
3894
  onOutcome,
3895
3895
  selectWorkflow,
3896
3896
  knownWorkflowIds,
3897
- frameworkAdapter
3897
+ frameworkAdapter,
3898
+ executionLeaseRelease
3898
3899
  }) {
3899
3900
  const out = new import_node_stream.PassThrough({ objectMode: true });
3900
3901
  const write = (chunk) => {
@@ -3912,11 +3913,13 @@ async function orchestrateGraphStream({
3912
3913
  let currentGraph = graph;
3913
3914
  let currentState = state;
3914
3915
  let finished = false;
3916
+ const leaseAbort = new AbortController();
3917
+ let releaseLease = executionLeaseRelease;
3915
3918
  const response = createResponseLifecycle({
3916
3919
  enabled: emitOutput,
3917
3920
  restored: initialConfig.responseCompleted === true,
3918
3921
  requestSignal,
3919
- executionSignal,
3922
+ executionSignal: executionSignal ? AbortSignal.any([executionSignal, leaseAbort.signal]) : leaseAbort.signal,
3920
3923
  onClosed: () => {
3921
3924
  out.write({ type: "done" });
3922
3925
  out.end();
@@ -4795,10 +4798,18 @@ async function orchestrateGraphStream({
4795
4798
  if (!traceAdapter?.withSpan) {
4796
4799
  emitTraceChunk();
4797
4800
  }
4798
- const runPromise = traceAdapter?.withSpan ? traceAdapter.withSpan(runSpanArgs, (runTraceSpan) => {
4801
+ const startRun = () => traceAdapter?.withSpan ? traceAdapter.withSpan(runSpanArgs, (runTraceSpan) => {
4799
4802
  emitTraceChunk();
4800
4803
  return runLoop(runTraceSpan);
4801
4804
  }) : runLoop(fallbackRunSpan ?? {});
4805
+ const runPromise = frameworkAdapter?.acquireRunLease && !releaseLease ? frameworkAdapter.acquireRunLease({
4806
+ runId,
4807
+ ...sessionId ? { sessionId } : {},
4808
+ onLost: (cause) => leaseAbort.abort(cause)
4809
+ }).then((release) => {
4810
+ releaseLease = release;
4811
+ return startRun();
4812
+ }) : startRun();
4802
4813
  const finishCancelled = async () => {
4803
4814
  emitTelemetryEvent({
4804
4815
  config,
@@ -4962,7 +4973,12 @@ async function orchestrateGraphStream({
4962
4973
  write({ type: "done" });
4963
4974
  finished = true;
4964
4975
  out.end();
4965
- }).finally(() => {
4976
+ }).finally(async () => {
4977
+ try {
4978
+ await releaseLease?.();
4979
+ } catch (error) {
4980
+ console.error("[execution:releaseLease]", (0, import_errors4.serializeFailure)(error));
4981
+ }
4966
4982
  response.dispose();
4967
4983
  onOutcome?.({
4968
4984
  state: outcomeState,
@@ -5023,7 +5039,7 @@ async function tryPrepareResumeStream({
5023
5039
  if (!meta) return null;
5024
5040
  const store = frameworkAdapter?.pendingRequests;
5025
5041
  if (!store) return null;
5026
- const pending = await store.get(meta.token);
5042
+ let pending = await store.get(meta.token);
5027
5043
  if (!pending || pending.requestId !== meta.requestId) {
5028
5044
  throw new ExecutionRequestError(
5029
5045
  "INVALID_RESUME",
@@ -5053,34 +5069,48 @@ async function tryPrepareResumeStream({
5053
5069
  yield { type: "done" };
5054
5070
  })();
5055
5071
  }
5056
- if (store.take && !await store.take(meta.token))
5057
- throw new ExecutionRequestError(
5058
- "INVALID_RESUME",
5059
- "Interrupt has already been resumed or cancelled."
5060
- );
5061
- if (meta.cancel) {
5062
- await store.delete(pending.token);
5063
- emitTelemetryEvent({
5064
- config,
5065
- type: "interrupt.cancelled",
5066
- correlation: {
5067
- runId: pending.runId,
5068
- sessionId,
5069
- workflowId: pending.workflow,
5070
- nodeId: pending.node
5071
- },
5072
- payload: {
5073
- interruptId: pending.requestId,
5074
- reason: "cancelled_by_client"
5075
- },
5076
- flush: true
5077
- });
5078
- onOutcome?.({ state: pending.state, cancelled: true });
5079
- return (async function* () {
5080
- yield { type: "done" };
5081
- })();
5082
- }
5072
+ const leaseAbort = new AbortController();
5073
+ let releaseLease;
5074
+ let consumed = false;
5083
5075
  try {
5076
+ releaseLease = await frameworkAdapter?.acquireRunLease?.({
5077
+ runId: pending.runId,
5078
+ sessionId,
5079
+ onLost: (cause) => leaseAbort.abort(cause)
5080
+ });
5081
+ leaseAbort.signal.throwIfAborted();
5082
+ if (store.take) {
5083
+ const taken = await store.take(meta.token);
5084
+ if (!taken)
5085
+ throw new ExecutionRequestError(
5086
+ "INVALID_RESUME",
5087
+ "Interrupt has already been resumed or cancelled."
5088
+ );
5089
+ pending = taken;
5090
+ }
5091
+ consumed = true;
5092
+ if (meta.cancel) {
5093
+ await store.delete(pending.token);
5094
+ emitTelemetryEvent({
5095
+ config,
5096
+ type: "interrupt.cancelled",
5097
+ correlation: {
5098
+ runId: pending.runId,
5099
+ sessionId,
5100
+ workflowId: pending.workflow,
5101
+ nodeId: pending.node
5102
+ },
5103
+ payload: {
5104
+ interruptId: pending.requestId,
5105
+ reason: "cancelled_by_client"
5106
+ },
5107
+ flush: true
5108
+ });
5109
+ onOutcome?.({ state: pending.state, cancelled: true });
5110
+ return (async function* () {
5111
+ yield { type: "done" };
5112
+ })();
5113
+ }
5084
5114
  const resumeData = isLimitPause ? {} : applyResumeSelection ? applyResumeSelection({ pending, selected: meta.selected }) : meta.selected?.length ? { coordinates: String(meta.selected[0]) } : {};
5085
5115
  const resumeDataPatch = isRecord4(resumeData) ? resumeData : {};
5086
5116
  const pendingMeta = isRecord4(pending.schema?.meta) ? pending.schema.meta : {};
@@ -5135,6 +5165,7 @@ async function tryPrepareResumeStream({
5135
5165
  ...resumeStatePatch
5136
5166
  };
5137
5167
  }
5168
+ leaseAbort.signal.throwIfAborted();
5138
5169
  if (pending.graphSnapshot && frameworkAdapter)
5139
5170
  await (0, import_runtime2.restoreGraphSnapshot)(
5140
5171
  frameworkAdapter.checkpointer,
@@ -5184,9 +5215,11 @@ async function tryPrepareResumeStream({
5184
5215
  },
5185
5216
  flush: true
5186
5217
  });
5218
+ const continuedSignal = pending.responseCompleted && emitOutput === false ? abortSignal : executionSignal;
5187
5219
  const args = {
5188
5220
  abortSignal,
5189
- executionSignal: pending.responseCompleted && emitOutput === false ? abortSignal : executionSignal,
5221
+ executionSignal: continuedSignal ? AbortSignal.any([continuedSignal, leaseAbort.signal]) : leaseAbort.signal,
5222
+ executionLeaseRelease: releaseLease,
5190
5223
  onExecution,
5191
5224
  emitOutput,
5192
5225
  onOutcome,
@@ -5207,11 +5240,15 @@ async function tryPrepareResumeStream({
5207
5240
  knownWorkflowIds,
5208
5241
  frameworkAdapter
5209
5242
  };
5243
+ leaseAbort.signal.throwIfAborted();
5210
5244
  const stream = await orchestrateGraphStream(args);
5245
+ releaseLease = void 0;
5211
5246
  return stream;
5212
5247
  } catch (error) {
5213
- await store.save(pending);
5248
+ if (consumed && !leaseAbort.signal.aborted) await store.save(pending);
5214
5249
  throw error;
5250
+ } finally {
5251
+ await releaseLease?.();
5215
5252
  }
5216
5253
  }
5217
5254
 
@@ -5907,30 +5944,38 @@ function createAgent(args) {
5907
5944
  const getCheckpoint = (id) => resolvedFrameworkAdapter.sessionCheckpoints.get(id);
5908
5945
  const rollbackTo = async (id) => {
5909
5946
  const target = await resolvedFrameworkAdapter.sessionCheckpoints.get(id);
5910
- const result = await resolvedFrameworkAdapter.sessionCheckpoints.rollbackTo(id);
5911
- const activePendingRequests = await Promise.all(
5912
- result.activePendingRequests.map(
5913
- (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
5914
- )
5915
- );
5916
- restoreWorkflowCallBranch(
5917
- activePendingRequests,
5918
- telemetry,
5919
- target?.runId ?? "unknown"
5920
- );
5921
- await Promise.all(
5922
- activePendingRequests.map(
5923
- (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
5924
- )
5947
+ const sourceRunId = target?.runId ?? "unknown";
5948
+ const managed = resolvedFrameworkAdapter.sessionCheckpoints.managesPendingRequests;
5949
+ const result = await resolvedFrameworkAdapter.sessionCheckpoints.rollbackTo(
5950
+ id,
5951
+ managed ? {
5952
+ preparePendingRequests: (requests) => restoreWorkflowCallBranch(requests, telemetry, sourceRunId)
5953
+ } : void 0
5925
5954
  );
5926
- await Promise.all([
5927
- ...result.invalidatedInterruptTokens.map(
5928
- (token) => resolvedFrameworkAdapter.pendingRequests.delete(token)
5929
- ),
5930
- ...activePendingRequests.map(
5931
- (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
5932
- )
5933
- ]);
5955
+ let activePendingRequests;
5956
+ if (managed) {
5957
+ activePendingRequests = result.activePendingRequests;
5958
+ } else {
5959
+ activePendingRequests = await Promise.all(
5960
+ result.activePendingRequests.map(
5961
+ (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
5962
+ )
5963
+ );
5964
+ restoreWorkflowCallBranch(activePendingRequests, telemetry, sourceRunId);
5965
+ await Promise.all(
5966
+ activePendingRequests.map(
5967
+ (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
5968
+ )
5969
+ );
5970
+ await Promise.all([
5971
+ ...result.invalidatedInterruptTokens.map(
5972
+ (token) => resolvedFrameworkAdapter.pendingRequests.delete(token)
5973
+ ),
5974
+ ...activePendingRequests.map(
5975
+ (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
5976
+ )
5977
+ ]);
5978
+ }
5934
5979
  if (target) {
5935
5980
  emitTelemetryEvent({
5936
5981
  config: { ...telemetry ? { telemetry } : {} },
@@ -5950,30 +5995,36 @@ function createAgent(args) {
5950
5995
  };
5951
5996
  const fork = async (id, options) => {
5952
5997
  const source = await resolvedFrameworkAdapter.sessionCheckpoints.get(id);
5998
+ const sourceRunId = source?.runId ?? "unknown";
5999
+ const managed = resolvedFrameworkAdapter.sessionCheckpoints.managesPendingRequests;
5953
6000
  const result = await resolvedFrameworkAdapter.sessionCheckpoints.fork(
5954
6001
  id,
5955
- options
5956
- );
5957
- const activePendingRequests = await Promise.all(
5958
- result.checkpoint.activePendingRequests.map(
5959
- (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
5960
- )
5961
- );
5962
- restoreWorkflowCallBranch(
5963
- activePendingRequests,
5964
- telemetry,
5965
- source?.runId ?? "unknown"
5966
- );
5967
- await Promise.all(
5968
- activePendingRequests.map(
5969
- (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
5970
- )
5971
- );
5972
- await Promise.all(
5973
- activePendingRequests.map(
5974
- (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
5975
- )
6002
+ managed ? {
6003
+ ...options,
6004
+ preparePendingRequests: (requests) => restoreWorkflowCallBranch(requests, telemetry, sourceRunId)
6005
+ } : options
5976
6006
  );
6007
+ let activePendingRequests;
6008
+ if (managed) {
6009
+ activePendingRequests = result.checkpoint.activePendingRequests;
6010
+ } else {
6011
+ activePendingRequests = await Promise.all(
6012
+ result.checkpoint.activePendingRequests.map(
6013
+ (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
6014
+ )
6015
+ );
6016
+ restoreWorkflowCallBranch(activePendingRequests, telemetry, sourceRunId);
6017
+ await Promise.all(
6018
+ activePendingRequests.map(
6019
+ (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
6020
+ )
6021
+ );
6022
+ await Promise.all(
6023
+ activePendingRequests.map(
6024
+ (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
6025
+ )
6026
+ );
6027
+ }
5977
6028
  emitTelemetryEvent({
5978
6029
  config: { ...telemetry ? { telemetry } : {} },
5979
6030
  type: "session.forked",