@kortyx/agent 0.22.2 → 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,35 @@
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
+
19
+ ## [0.23.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.22.2...agent-v0.23.0) (2026-09-16)
20
+
21
+
22
+ ### Features
23
+
24
+ * **runtime:** support durable child calls in parallel DAGs ([#203](https://github.com/kortyx-io/kortyx/issues/203)) ([525e0e7](https://github.com/kortyx-io/kortyx/commit/525e0e7a73fa5f9d78232ca3155d86be7bc1022d))
25
+
26
+
27
+ ### Dependencies
28
+
29
+ * The following workspace dependencies were updated
30
+ * dependencies
31
+ * @kortyx/runtime bumped to 0.18.0
32
+
3
33
  ## [0.22.2](https://github.com/kortyx-io/kortyx/compare/agent-v0.22.1...agent-v0.22.2) (2026-09-15)
4
34
 
5
35
 
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
@@ -3540,6 +3540,7 @@ async function* transformGraphStreamForUI(stream, options = {}) {
3540
3540
  let latestGraphState = null;
3541
3541
  for await (const event of stream) {
3542
3542
  const { event: type, name, data } = event ?? {};
3543
+ if (name?.startsWith("__kortyx_parallel_")) continue;
3543
3544
  if (debug) console.log(`[debug:event]`, JSON.stringify(event, null, 2));
3544
3545
  switch (type) {
3545
3546
  case "on_chain_start":
@@ -3893,10 +3894,15 @@ async function orchestrateGraphStream({
3893
3894
  onOutcome,
3894
3895
  selectWorkflow,
3895
3896
  knownWorkflowIds,
3896
- frameworkAdapter
3897
+ frameworkAdapter,
3898
+ executionLeaseRelease
3897
3899
  }) {
3898
3900
  const out = new import_node_stream.PassThrough({ objectMode: true });
3899
3901
  const write = (chunk) => {
3902
+ if (isRecord3(chunk) && chunk.type === "done" && isGraphState(chunk.data)) {
3903
+ const { __kortyxParallelGraph: _journal, ...runtime } = chunk.data.runtime;
3904
+ chunk = { ...chunk, data: { ...chunk.data, runtime } };
3905
+ }
3900
3906
  if (emitOutput && !response.closed && !out.destroyed) out.write(chunk);
3901
3907
  };
3902
3908
  let outcomeState = state;
@@ -3907,11 +3913,13 @@ async function orchestrateGraphStream({
3907
3913
  let currentGraph = graph;
3908
3914
  let currentState = state;
3909
3915
  let finished = false;
3916
+ const leaseAbort = new AbortController();
3917
+ let releaseLease = executionLeaseRelease;
3910
3918
  const response = createResponseLifecycle({
3911
3919
  enabled: emitOutput,
3912
3920
  restored: initialConfig.responseCompleted === true,
3913
3921
  requestSignal,
3914
- executionSignal,
3922
+ executionSignal: executionSignal ? AbortSignal.any([executionSignal, leaseAbort.signal]) : leaseAbort.signal,
3915
3923
  onClosed: () => {
3916
3924
  out.write({ type: "done" });
3917
3925
  out.end();
@@ -4226,6 +4234,8 @@ async function orchestrateGraphStream({
4226
4234
  return;
4227
4235
  }
4228
4236
  if (event === "status") {
4237
+ const statusNode = payload?.node;
4238
+ if (typeof statusNode === "string") touchedNodes.add(statusNode);
4229
4239
  if (!debugEnabled) return;
4230
4240
  const statusMessage = payload?.message;
4231
4241
  const msg = statusMessage === null || statusMessage === void 0 ? "" : String(statusMessage);
@@ -4788,10 +4798,18 @@ async function orchestrateGraphStream({
4788
4798
  if (!traceAdapter?.withSpan) {
4789
4799
  emitTraceChunk();
4790
4800
  }
4791
- const runPromise = traceAdapter?.withSpan ? traceAdapter.withSpan(runSpanArgs, (runTraceSpan) => {
4801
+ const startRun = () => traceAdapter?.withSpan ? traceAdapter.withSpan(runSpanArgs, (runTraceSpan) => {
4792
4802
  emitTraceChunk();
4793
4803
  return runLoop(runTraceSpan);
4794
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();
4795
4813
  const finishCancelled = async () => {
4796
4814
  emitTelemetryEvent({
4797
4815
  config,
@@ -4821,6 +4839,12 @@ async function orchestrateGraphStream({
4821
4839
  out.end();
4822
4840
  };
4823
4841
  const completion = runPromise.catch(async (err) => {
4842
+ const runtimePatch = err?.__kortyxHookStatePatch;
4843
+ if (runtimePatch)
4844
+ outcomeState = {
4845
+ ...outcomeState,
4846
+ runtime: { ...outcomeState.runtime, ...runtimePatch }
4847
+ };
4824
4848
  if (abortSignal?.aborted || (0, import_core2.isExecutionCancelled)(err)) {
4825
4849
  await finishCancelled();
4826
4850
  return;
@@ -4949,7 +4973,12 @@ async function orchestrateGraphStream({
4949
4973
  write({ type: "done" });
4950
4974
  finished = true;
4951
4975
  out.end();
4952
- }).finally(() => {
4976
+ }).finally(async () => {
4977
+ try {
4978
+ await releaseLease?.();
4979
+ } catch (error) {
4980
+ console.error("[execution:releaseLease]", (0, import_errors4.serializeFailure)(error));
4981
+ }
4953
4982
  response.dispose();
4954
4983
  onOutcome?.({
4955
4984
  state: outcomeState,
@@ -5010,7 +5039,7 @@ async function tryPrepareResumeStream({
5010
5039
  if (!meta) return null;
5011
5040
  const store = frameworkAdapter?.pendingRequests;
5012
5041
  if (!store) return null;
5013
- const pending = await store.get(meta.token);
5042
+ let pending = await store.get(meta.token);
5014
5043
  if (!pending || pending.requestId !== meta.requestId) {
5015
5044
  throw new ExecutionRequestError(
5016
5045
  "INVALID_RESUME",
@@ -5040,34 +5069,48 @@ async function tryPrepareResumeStream({
5040
5069
  yield { type: "done" };
5041
5070
  })();
5042
5071
  }
5043
- if (store.take && !await store.take(meta.token))
5044
- throw new ExecutionRequestError(
5045
- "INVALID_RESUME",
5046
- "Interrupt has already been resumed or cancelled."
5047
- );
5048
- if (meta.cancel) {
5049
- await store.delete(pending.token);
5050
- emitTelemetryEvent({
5051
- config,
5052
- type: "interrupt.cancelled",
5053
- correlation: {
5054
- runId: pending.runId,
5055
- sessionId,
5056
- workflowId: pending.workflow,
5057
- nodeId: pending.node
5058
- },
5059
- payload: {
5060
- interruptId: pending.requestId,
5061
- reason: "cancelled_by_client"
5062
- },
5063
- flush: true
5064
- });
5065
- onOutcome?.({ state: pending.state, cancelled: true });
5066
- return (async function* () {
5067
- yield { type: "done" };
5068
- })();
5069
- }
5072
+ const leaseAbort = new AbortController();
5073
+ let releaseLease;
5074
+ let consumed = false;
5070
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
+ }
5071
5114
  const resumeData = isLimitPause ? {} : applyResumeSelection ? applyResumeSelection({ pending, selected: meta.selected }) : meta.selected?.length ? { coordinates: String(meta.selected[0]) } : {};
5072
5115
  const resumeDataPatch = isRecord4(resumeData) ? resumeData : {};
5073
5116
  const pendingMeta = isRecord4(pending.schema?.meta) ? pending.schema.meta : {};
@@ -5122,6 +5165,7 @@ async function tryPrepareResumeStream({
5122
5165
  ...resumeStatePatch
5123
5166
  };
5124
5167
  }
5168
+ leaseAbort.signal.throwIfAborted();
5125
5169
  if (pending.graphSnapshot && frameworkAdapter)
5126
5170
  await (0, import_runtime2.restoreGraphSnapshot)(
5127
5171
  frameworkAdapter.checkpointer,
@@ -5171,9 +5215,11 @@ async function tryPrepareResumeStream({
5171
5215
  },
5172
5216
  flush: true
5173
5217
  });
5218
+ const continuedSignal = pending.responseCompleted && emitOutput === false ? abortSignal : executionSignal;
5174
5219
  const args = {
5175
5220
  abortSignal,
5176
- executionSignal: pending.responseCompleted && emitOutput === false ? abortSignal : executionSignal,
5221
+ executionSignal: continuedSignal ? AbortSignal.any([continuedSignal, leaseAbort.signal]) : leaseAbort.signal,
5222
+ executionLeaseRelease: releaseLease,
5177
5223
  onExecution,
5178
5224
  emitOutput,
5179
5225
  onOutcome,
@@ -5194,11 +5240,15 @@ async function tryPrepareResumeStream({
5194
5240
  knownWorkflowIds,
5195
5241
  frameworkAdapter
5196
5242
  };
5243
+ leaseAbort.signal.throwIfAborted();
5197
5244
  const stream = await orchestrateGraphStream(args);
5245
+ releaseLease = void 0;
5198
5246
  return stream;
5199
5247
  } catch (error) {
5200
- await store.save(pending);
5248
+ if (consumed && !leaseAbort.signal.aborted) await store.save(pending);
5201
5249
  throw error;
5250
+ } finally {
5251
+ await releaseLease?.();
5202
5252
  }
5203
5253
  }
5204
5254
 
@@ -5894,30 +5944,38 @@ function createAgent(args) {
5894
5944
  const getCheckpoint = (id) => resolvedFrameworkAdapter.sessionCheckpoints.get(id);
5895
5945
  const rollbackTo = async (id) => {
5896
5946
  const target = await resolvedFrameworkAdapter.sessionCheckpoints.get(id);
5897
- const result = await resolvedFrameworkAdapter.sessionCheckpoints.rollbackTo(id);
5898
- const activePendingRequests = await Promise.all(
5899
- result.activePendingRequests.map(
5900
- (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
5901
- )
5902
- );
5903
- restoreWorkflowCallBranch(
5904
- activePendingRequests,
5905
- telemetry,
5906
- target?.runId ?? "unknown"
5907
- );
5908
- await Promise.all(
5909
- activePendingRequests.map(
5910
- (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
5911
- )
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
5912
5954
  );
5913
- await Promise.all([
5914
- ...result.invalidatedInterruptTokens.map(
5915
- (token) => resolvedFrameworkAdapter.pendingRequests.delete(token)
5916
- ),
5917
- ...activePendingRequests.map(
5918
- (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
5919
- )
5920
- ]);
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
+ }
5921
5979
  if (target) {
5922
5980
  emitTelemetryEvent({
5923
5981
  config: { ...telemetry ? { telemetry } : {} },
@@ -5937,30 +5995,36 @@ function createAgent(args) {
5937
5995
  };
5938
5996
  const fork = async (id, options) => {
5939
5997
  const source = await resolvedFrameworkAdapter.sessionCheckpoints.get(id);
5998
+ const sourceRunId = source?.runId ?? "unknown";
5999
+ const managed = resolvedFrameworkAdapter.sessionCheckpoints.managesPendingRequests;
5940
6000
  const result = await resolvedFrameworkAdapter.sessionCheckpoints.fork(
5941
6001
  id,
5942
- options
5943
- );
5944
- const activePendingRequests = await Promise.all(
5945
- result.checkpoint.activePendingRequests.map(
5946
- (request) => hydratePendingGraphCheckpoint(resolvedFrameworkAdapter, request)
5947
- )
5948
- );
5949
- restoreWorkflowCallBranch(
5950
- activePendingRequests,
5951
- telemetry,
5952
- source?.runId ?? "unknown"
5953
- );
5954
- await Promise.all(
5955
- activePendingRequests.map(
5956
- (request) => clearPendingGraphWrites(resolvedFrameworkAdapter, request)
5957
- )
5958
- );
5959
- await Promise.all(
5960
- activePendingRequests.map(
5961
- (request) => resolvedFrameworkAdapter.pendingRequests.save(request)
5962
- )
6002
+ managed ? {
6003
+ ...options,
6004
+ preparePendingRequests: (requests) => restoreWorkflowCallBranch(requests, telemetry, sourceRunId)
6005
+ } : options
5963
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
+ }
5964
6028
  emitTelemetryEvent({
5965
6029
  config: { ...telemetry ? { telemetry } : {} },
5966
6030
  type: "session.forked",