@kortyx/hooks 0.19.0 → 0.21.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.21.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.20.0...hooks-v0.21.0) (2026-09-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * **runtime:** add shared execution limits and checkpoint continuation ([#163](https://github.com/kortyx-io/kortyx/issues/163)) ([e22232c](https://github.com/kortyx-io/kortyx/commit/e22232c059fae4f7df1bd36423902bd365887942))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @kortyx/core bumped to 0.8.0
16
+ * @kortyx/telemetry-contracts bumped to 0.4.0
17
+
18
+ ## [0.20.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.19.0...hooks-v0.20.0) (2026-09-10)
19
+
20
+
21
+ ### Features
22
+
23
+ * cancel workflow trees and reflect cancellation in Studio ([#161](https://github.com/kortyx-io/kortyx/issues/161)) ([699ad18](https://github.com/kortyx-io/kortyx/commit/699ad18905955c6340c3d85b9fae7f0bffd00a0d))
24
+ * execute and resume workflows without chat transport ([#159](https://github.com/kortyx-io/kortyx/issues/159)) ([6decb17](https://github.com/kortyx-io/kortyx/commit/6decb1714fedc021ac9e125b6ed9f75475ed928a))
25
+
26
+
27
+ ### Dependencies
28
+
29
+ * The following workspace dependencies were updated
30
+ * dependencies
31
+ * @kortyx/core bumped to 0.7.0
32
+
3
33
  ## [0.19.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.18.0...hooks-v0.19.0) (2026-09-09)
4
34
 
5
35
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as _kortyx_core from '@kortyx/core';
1
2
  import { WorkflowDefinition, TokenUsage, InterruptInput, NodeConfig, InterruptResult, GraphState } from '@kortyx/core';
2
3
  import { K as KortyxTelemetryConfig, R as ReasonTraceAdapter, a as KortyxTraceMetadata, b as KortyxTelemetryEventType } from './tracing-BI6YKHSf.js';
3
4
  export { E as EnsureWorkflowTopologyRequest, c as EnsureWorkflowTopologyResponse, d as KortyxTelemetryContentCapture, e as KortyxTelemetryCorrelation, f as KortyxTelemetryEvent, g as KortyxTelemetryPrompt, h as KortyxTelemetryReporter, i as KortyxTelemetryService, j as KortyxTraceAdapter, k as ReasonTraceAttributes, l as ReasonTraceSpan, m as ReasonTraceSpanEndArgs, n as ReasonTraceSpanStartArgs } from './tracing-BI6YKHSf.js';
@@ -13,6 +14,7 @@ type WorkflowCallOutcome = {
13
14
  status: "interrupted";
14
15
  request: InterruptInput;
15
16
  snapshot: unknown;
17
+ usage?: TokenUsage | undefined;
16
18
  };
17
19
  type WorkflowCallService = (args: {
18
20
  id: string;
@@ -60,6 +62,9 @@ declare function useWorkflow(args: {
60
62
  }>;
61
63
 
62
64
  type HookNodeRuntimeContext = {
65
+ /** Transient execution control; never part of checkpointed state. */
66
+ abortSignal?: AbortSignal | undefined;
67
+ consumeExecution?: ((limit: _kortyx_core.ExecutionLimit) => void) | undefined;
63
68
  graph: {
64
69
  name: string;
65
70
  node: string;
@@ -214,6 +219,8 @@ type StateSetter<T> = (next: T | ((prev: T) => T)) => void;
214
219
  declare function useStructuredData<TData = unknown>(args: UseStructuredDataArgs<TData>): void;
215
220
  declare function useReason<TOutput = unknown, TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult>(args: UseReasonArgs<TOutput, TRequest, TResponse>): Promise<UseReasonResult<TOutput, TResponse>>;
216
221
  declare function useInterrupt<TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult>(args: UseInterruptArgs<TRequest, TResponse>): Promise<TResponse>;
222
+ /** Live execution signal for cooperative I/O. Never store it in workflow state. */
223
+ declare function useAbortSignal(): AbortSignal | undefined;
217
224
  declare function useRuntimeContext<TContext extends Record<string, unknown> = Record<string, unknown>>(): TContext;
218
225
  declare function useNodeState<T>(initialValue: T): [T, StateSetter<T>];
219
226
  declare function useWorkflowState<T>(key: string, ...rest: [] | [T]): [T, StateSetter<T>];
@@ -221,4 +228,4 @@ declare function useWorkflowState<T>(key: string, ...rest: [] | [T]): [T, StateS
221
228
  /** Best effort lifecycle facts. Engine snapshots and interrupt tokens never leave the runtime. */
222
229
  declare function emitWorkflowCall(telemetry: KortyxTelemetryConfig | undefined, type: KortyxTelemetryEventType, payload: Record<string, unknown>): void;
223
230
 
224
- export { type HookRuntimeContext, KortyxTelemetryConfig, KortyxTelemetryEventType, KortyxTraceMetadata, ReasonTraceAdapter, type UseInterruptArgs, type UseReasonArgs, type UseReasonResult, type UseReasonStep, type UseReasonToolExecution, type UseStructuredDataArgs, WorkflowCallError, type WorkflowCallOutcome, type WorkflowCallService, createWorkflowHooks, emitWorkflowCall, runWithHookContext, useInterrupt, useNodeState, useReason, useRuntimeContext, useStructuredData, useWorkflow, useWorkflowState, workflowCallFingerprint };
231
+ export { type HookRuntimeContext, KortyxTelemetryConfig, KortyxTelemetryEventType, KortyxTraceMetadata, ReasonTraceAdapter, type UseInterruptArgs, type UseReasonArgs, type UseReasonResult, type UseReasonStep, type UseReasonToolExecution, type UseStructuredDataArgs, WorkflowCallError, type WorkflowCallOutcome, type WorkflowCallService, createWorkflowHooks, emitWorkflowCall, runWithHookContext, useAbortSignal, useInterrupt, useNodeState, useReason, useRuntimeContext, useStructuredData, useWorkflow, useWorkflowState, workflowCallFingerprint };
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  createWorkflowHooks: () => createWorkflowHooks,
24
24
  emitWorkflowCall: () => emitWorkflowCall,
25
25
  runWithHookContext: () => runWithHookContext,
26
+ useAbortSignal: () => useAbortSignal,
26
27
  useInterrupt: () => useInterrupt,
27
28
  useNodeState: () => useNodeState,
28
29
  useReason: () => useReason2,
@@ -188,6 +189,9 @@ function getHookContext() {
188
189
  return ctx;
189
190
  }
190
191
 
192
+ // src/interrupt.ts
193
+ var import_core = require("@kortyx/core");
194
+
191
195
  // src/reason/checkpoint.ts
192
196
  var resolveReasonCheckpointKey = (args) => typeof args.id === "string" && args.id.length > 0 ? `__useReason:${args.id}` : `__useReason:auto:${args.autoIndex}`;
193
197
  var readReasonCheckpoint = (raw) => {
@@ -243,6 +247,7 @@ var parseWithSchema = (schema, value, label) => {
243
247
  // src/interrupt.ts
244
248
  var awaitInterruptInternal = (args) => {
245
249
  const ctx = getHookContext();
250
+ (0, import_core.throwIfExecutionAborted)(ctx.node.abortSignal);
246
251
  const request = parseWithSchema(
247
252
  args.requestSchema,
248
253
  args.request,
@@ -276,6 +281,9 @@ var awaitInterruptInternal = (args) => {
276
281
  return Promise.resolve(response);
277
282
  };
278
283
 
284
+ // src/reason/use-reason.ts
285
+ var import_core5 = require("@kortyx/core");
286
+
279
287
  // src/structured.ts
280
288
  var import_node_crypto = require("crypto");
281
289
 
@@ -370,9 +378,11 @@ var shouldStreamStructured = (cfg) => Boolean(cfg) && (cfg?.stream ?? true);
370
378
 
371
379
  // src/reason/engine.ts
372
380
  var import_node_crypto3 = require("crypto");
381
+ var import_core3 = require("@kortyx/core");
373
382
 
374
383
  // src/reason-engine.ts
375
384
  var import_node_crypto2 = require("crypto");
385
+ var import_core2 = require("@kortyx/core");
376
386
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
377
387
  var createRuntimeId = () => {
378
388
  try {
@@ -399,6 +409,7 @@ async function runReasonEngine(args) {
399
409
  const responseFormat = args.responseFormat ?? args.model.options?.responseFormat;
400
410
  const providerOptions = args.providerOptions ?? args.model.options?.providerOptions;
401
411
  const tools = args.tools ?? args.model.options?.tools;
412
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
402
413
  const model = args.model.provider.getModel(args.model.modelId, {
403
414
  ...temperature !== void 0 ? { temperature } : {},
404
415
  streaming: stream,
@@ -491,6 +502,7 @@ async function runReasonEngine(args) {
491
502
  }
492
503
  const response2 = await model.stream(messages);
493
504
  for await (const chunk of response2) {
505
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
494
506
  switch (chunk.type) {
495
507
  case "text-delta": {
496
508
  if (chunk.delta.length === 0) break;
@@ -569,10 +581,12 @@ async function runReasonEngine(args) {
569
581
  ...providerMetadata !== void 0 ? { providerMetadata } : {},
570
582
  ...warnings !== void 0 ? { warnings } : {}
571
583
  };
584
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
572
585
  endTrace(traceSpan, result2);
573
586
  return result2;
574
587
  }
575
588
  const response = await model.invoke(messages);
589
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
576
590
  if (emit) {
577
591
  emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
578
592
  ...commonMeta
@@ -599,8 +613,9 @@ async function runReasonEngine(args) {
599
613
  endTrace(traceSpan, result);
600
614
  return result;
601
615
  } catch (error) {
602
- failTrace(traceSpan, error);
603
- throw error;
616
+ const failure = abortSignal?.aborted ? (0, import_core2.createExecutionCancelledError)() : error;
617
+ failTrace(traceSpan, failure);
618
+ throw failure;
604
619
  }
605
620
  }
606
621
 
@@ -614,6 +629,13 @@ var createRuntimeId2 = () => {
614
629
  };
615
630
  async function reasonEngine(args, meta, inputOverride) {
616
631
  const ctx = getHookContext();
632
+ const abortSignal = (0, import_core3.combineAbortSignals)(
633
+ ctx.node.abortSignal,
634
+ args.abortSignal,
635
+ args.model.options?.abortSignal
636
+ );
637
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
638
+ ctx.node.consumeExecution?.("maxModelPasses");
617
639
  return runReasonEngine({
618
640
  model: args.model,
619
641
  input: inputOverride ?? args.input,
@@ -621,7 +643,7 @@ async function reasonEngine(args, meta, inputOverride) {
621
643
  temperature: args.temperature,
622
644
  maxOutputTokens: args.maxOutputTokens,
623
645
  stopSequences: args.stopSequences,
624
- abortSignal: args.abortSignal,
646
+ abortSignal,
625
647
  reasoning: args.reasoning,
626
648
  responseFormat: args.responseFormat,
627
649
  providerOptions: args.providerOptions,
@@ -1572,6 +1594,7 @@ var extractStreamingStringValues = (args) => {
1572
1594
  };
1573
1595
 
1574
1596
  // src/reason/tools.ts
1597
+ var import_core4 = require("@kortyx/core");
1575
1598
  var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
1576
1599
  var toToolDefinitions = (tools) => tools.map((tool) => ({
1577
1600
  name: tool.name,
@@ -1625,6 +1648,11 @@ var createInitialMessages = (args) => [
1625
1648
  var runReasonToolLoop = async (args) => {
1626
1649
  const { useReasonArgs, id, opId, traceSpan } = args;
1627
1650
  const ctx = getHookContext();
1651
+ const abortSignal = (0, import_core4.combineAbortSignals)(
1652
+ ctx.node.abortSignal,
1653
+ useReasonArgs.abortSignal,
1654
+ useReasonArgs.model.options?.abortSignal
1655
+ );
1628
1656
  const tools = useReasonArgs.tools ?? [];
1629
1657
  const toolByName = /* @__PURE__ */ new Map();
1630
1658
  let validationCompleted = false;
@@ -1674,6 +1702,7 @@ var runReasonToolLoop = async (args) => {
1674
1702
  };
1675
1703
  try {
1676
1704
  for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
1705
+ (0, import_core4.throwIfExecutionAborted)(abortSignal);
1677
1706
  traceSpan?.addEvent?.("useReason.tool-step.start", {
1678
1707
  stepIndex,
1679
1708
  toolCount: toolDefinitions.length
@@ -1691,6 +1720,7 @@ var runReasonToolLoop = async (args) => {
1691
1720
  finalText = step.text;
1692
1721
  finalRaw = step.raw;
1693
1722
  aggregatedUsage = mergeUsage(aggregatedUsage, step.usage);
1723
+ accumulateTokenUsage(step.usage);
1694
1724
  finalFinishReason = step.finishReason;
1695
1725
  aggregatedProviderMetadata = mergeProviderMetadata(
1696
1726
  aggregatedProviderMetadata,
@@ -1724,6 +1754,7 @@ var runReasonToolLoop = async (args) => {
1724
1754
  toolCalls
1725
1755
  });
1726
1756
  for (const toolCall of toolCalls) {
1757
+ (0, import_core4.throwIfExecutionAborted)(abortSignal);
1727
1758
  const tool = toolByName.get(toolCall.name);
1728
1759
  if (!tool) {
1729
1760
  throw new Error(
@@ -1791,10 +1822,13 @@ var runReasonToolLoop = async (args) => {
1791
1822
  }
1792
1823
  }
1793
1824
  try {
1825
+ (0, import_core4.throwIfExecutionAborted)(abortSignal);
1826
+ ctx.node.consumeExecution?.("maxToolCalls");
1794
1827
  const rawResult = await tool.execute(toolCall.input, {
1795
1828
  toolCallId: toolCall.id,
1796
- ...useReasonArgs.abortSignal ? { abortSignal: useReasonArgs.abortSignal } : {}
1829
+ ...abortSignal ? { abortSignal } : {}
1797
1830
  });
1831
+ (0, import_core4.throwIfExecutionAborted)(abortSignal);
1798
1832
  const result2 = normalizeToolResult(toolCall, rawResult);
1799
1833
  toolResults.push(result2);
1800
1834
  allToolResults.push(result2);
@@ -1814,6 +1848,9 @@ var runReasonToolLoop = async (args) => {
1814
1848
  isError: Boolean(result2.isError)
1815
1849
  });
1816
1850
  } catch (error) {
1851
+ (0, import_core4.throwIfExecutionAborted)(abortSignal);
1852
+ if ((0, import_core4.isExecutionLimitReached)(error) || (0, import_core4.isExecutionCancelled)(error))
1853
+ throw error;
1817
1854
  const result2 = {
1818
1855
  toolCallId: toolCall.id,
1819
1856
  name: tool.name,
@@ -1937,6 +1974,13 @@ var assertReasoningThoughtsCompatibility = (args) => {
1937
1974
  async function useReason(args) {
1938
1975
  assertReasoningThoughtsCompatibility(args);
1939
1976
  const ctx = getHookContext();
1977
+ const abortSignal = (0, import_core5.combineAbortSignals)(
1978
+ ctx.node.abortSignal,
1979
+ args.abortSignal,
1980
+ args.model.options?.abortSignal
1981
+ );
1982
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
1983
+ args = { ...args, abortSignal };
1940
1984
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
1941
1985
  const opId = createRuntimeId2();
1942
1986
  const checkpointKey = resolveReasonCheckpointKey({
@@ -2306,6 +2350,9 @@ function useReason2(args) {
2306
2350
  function useInterrupt(args) {
2307
2351
  return awaitInterruptInternal(args);
2308
2352
  }
2353
+ function useAbortSignal() {
2354
+ return getHookContext().node.abortSignal;
2355
+ }
2309
2356
  function useRuntimeContext() {
2310
2357
  const ctx = getHookContext();
2311
2358
  const config = ctx.state.config;
@@ -2350,6 +2397,7 @@ function useWorkflowState(key, ...rest) {
2350
2397
 
2351
2398
  // src/workflow.ts
2352
2399
  var import_node_crypto5 = require("crypto");
2400
+ var import_core6 = require("@kortyx/core");
2353
2401
 
2354
2402
  // src/workflow-telemetry.ts
2355
2403
  var import_node_crypto4 = require("crypto");
@@ -2436,6 +2484,7 @@ function createWorkflowHooks(workflows) {
2436
2484
  }
2437
2485
  async function useWorkflow(args) {
2438
2486
  const ctx = getHookContext();
2487
+ (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2439
2488
  const service = ctx.node.callWorkflow;
2440
2489
  if (!service)
2441
2490
  throw new Error("useWorkflow requires an agent workflow registry.");
@@ -2469,6 +2518,7 @@ async function useWorkflow(args) {
2469
2518
  workflow,
2470
2519
  "call input or target changed during replay"
2471
2520
  );
2521
+ if (!record) ctx.node.consumeExecution?.("maxChildInvocations");
2472
2522
  record ??= {
2473
2523
  fingerprint,
2474
2524
  invocationId: (0, import_node_crypto5.randomUUID)(),
@@ -2502,6 +2552,15 @@ async function useWorkflow(args) {
2502
2552
  ...content
2503
2553
  });
2504
2554
  };
2555
+ const recordUsage = (usage) => {
2556
+ if (!usage) return;
2557
+ accumulateTokenUsage({
2558
+ input: usage.input - (record.usage?.input ?? 0),
2559
+ output: usage.output - (record.usage?.output ?? 0),
2560
+ total: usage.total - (record.usage?.total ?? 0)
2561
+ });
2562
+ record.usage = usage;
2563
+ };
2505
2564
  const cached = record.status === "completed";
2506
2565
  try {
2507
2566
  for (const interruption of record.interrupts) {
@@ -2530,14 +2589,28 @@ async function useWorkflow(args) {
2530
2589
  ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
2531
2590
  });
2532
2591
  } catch (error) {
2592
+ (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2593
+ if ((0, import_core6.isExecutionCancelled)(error)) throw error;
2594
+ if ((0, import_core6.isExecutionLimitReached)(error)) {
2595
+ const child = error;
2596
+ if (child.__kortyxChildSnapshot)
2597
+ record.snapshot = child.__kortyxChildSnapshot;
2598
+ recordUsage(child.__kortyxChildUsage);
2599
+ report("suspended", {
2600
+ reason: "limit_reached",
2601
+ limit: error.limitReached
2602
+ });
2603
+ throw error;
2604
+ }
2533
2605
  record.status = "failed";
2534
2606
  record.error = error instanceof Error ? error.message : String(error);
2535
2607
  report("failed", { error: "Child workflow failed" });
2536
2608
  throw new WorkflowCallError(workflow, record.error);
2537
2609
  }
2610
+ (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2611
+ recordUsage(outcome.usage);
2538
2612
  if (outcome.status === "completed") {
2539
2613
  record.data = outcome.data;
2540
- accumulateTokenUsage(outcome.usage);
2541
2614
  workflowCallFingerprint(record.data);
2542
2615
  record.status = "completed";
2543
2616
  delete record.snapshot;
@@ -2577,6 +2650,7 @@ async function useWorkflow(args) {
2577
2650
  createWorkflowHooks,
2578
2651
  emitWorkflowCall,
2579
2652
  runWithHookContext,
2653
+ useAbortSignal,
2580
2654
  useInterrupt,
2581
2655
  useNodeState,
2582
2656
  useReason,