@kortyx/hooks 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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.18.0...hooks-v0.19.0) (2026-09-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * add resumable child workflows and Studio inspection ([#157](https://github.com/kortyx-io/kortyx/issues/157)) ([542ba29](https://github.com/kortyx-io/kortyx/commit/542ba29b3b57217e6e5d98c1b409bbec5ce3261f))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @kortyx/core bumped to 0.6.0
16
+ * @kortyx/telemetry-contracts bumped to 0.3.0
17
+
3
18
  ## [0.18.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.17.1...hooks-v0.18.0) (2026-08-02)
4
19
 
5
20
 
package/README.md CHANGED
@@ -53,3 +53,31 @@ Workarounds:
53
53
  ## License
54
54
 
55
55
  Apache-2.0. See [LICENSE](https://github.com/kortyx-io/kortyx/blob/main/LICENSE).
56
+
57
+
58
+ ## Child workflows
59
+
60
+ Call a registered workflow from a node or custom hook and continue with its
61
+ result:
62
+
63
+ ```ts
64
+ const result = await useWorkflow({
65
+ id: "research",
66
+ workflow: researchWorkflow,
67
+ input: { topic: "AI agents" },
68
+ });
69
+ return { data: { summary: result.data.summary } };
70
+ ```
71
+
72
+ The child defines `inputSchema` and `outputSchema` on `defineWorkflow`.
73
+ Inputs and returned data are inferred from those schemas and validated at
74
+ runtime. The result is the child's accumulated data parsed by its output
75
+ schema. Use `createWorkflowHooks({ research: researchWorkflow })` for typed
76
+ string ids. Register the same definitions with the agent.
77
+
78
+ Calls need stable ids. A child interrupt pauses the chain; resuming replays
79
+ the enclosing node and reuses saved child results. Keep effects before calls
80
+ idempotent. Fork/rollback retain nested checkpoints; use Redis for persistence
81
+ across server restarts. Concurrent calls are unsupported.
82
+
83
+ See [the execution and replay contract](../../docs/design-specs/child-workflows.md).
package/dist/index.d.ts CHANGED
@@ -1,9 +1,64 @@
1
- import { NodeConfig, InterruptInput, InterruptResult, GraphState } from '@kortyx/core';
2
- import { R as ReasonTraceAdapter, K as KortyxTraceMetadata } from './tracing-DeYHBpY1.js';
3
- export { E as EnsureWorkflowTopologyRequest, a as EnsureWorkflowTopologyResponse, b as KortyxTelemetryConfig, c as KortyxTelemetryContentCapture, d as KortyxTelemetryCorrelation, e as KortyxTelemetryEvent, f as KortyxTelemetryEventType, 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-DeYHBpY1.js';
1
+ import { WorkflowDefinition, TokenUsage, InterruptInput, NodeConfig, InterruptResult, GraphState } from '@kortyx/core';
2
+ import { K as KortyxTelemetryConfig, R as ReasonTraceAdapter, a as KortyxTraceMetadata, b as KortyxTelemetryEventType } from './tracing-BI6YKHSf.js';
3
+ 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';
4
+ import { z } from 'zod';
4
5
  import { ProviderModelRef, KortyxReasoningOptions, KortyxResponseFormat, KortyxExecutableTool, KortyxUsage, KortyxFinishReason, KortyxProviderMetadata, KortyxWarning, KortyxToolCall, KortyxToolResult } from '@kortyx/providers';
5
6
  import '@kortyx/telemetry-contracts';
6
7
 
8
+ type WorkflowCallOutcome = {
9
+ status: "completed";
10
+ data: Record<string, unknown>;
11
+ usage?: TokenUsage | undefined;
12
+ } | {
13
+ status: "interrupted";
14
+ request: InterruptInput;
15
+ snapshot: unknown;
16
+ };
17
+ type WorkflowCallService = (args: {
18
+ id: string;
19
+ workflow: string;
20
+ input: unknown;
21
+ snapshot?: unknown;
22
+ definition?: WorkflowDefinition | undefined;
23
+ invocationId: string;
24
+ response?: string | string[] | undefined;
25
+ callerNodeExecutionId?: string | undefined;
26
+ }) => Promise<WorkflowCallOutcome>;
27
+ declare class WorkflowCallError extends Error {
28
+ readonly workflow: string;
29
+ constructor(workflow: string, message: string);
30
+ }
31
+ declare function workflowCallFingerprint(value: unknown): string;
32
+ type SchematizedWorkflow = WorkflowDefinition & {
33
+ inputSchema: z.ZodType;
34
+ outputSchema: z.ZodType<Record<string, unknown>>;
35
+ };
36
+ /** Bind string workflow ids to the same definitions registered with the agent. */
37
+ declare function createWorkflowHooks<const W extends Record<string, SchematizedWorkflow>>(workflows: W): {
38
+ useWorkflow: <K extends keyof W & string>(args: {
39
+ id: string;
40
+ workflow: K;
41
+ input: z.input<W[K]["inputSchema"]>;
42
+ }) => Promise<{
43
+ data: z.output<W[K]["outputSchema"]>;
44
+ }>;
45
+ };
46
+ /** Typed references avoid caller-supplied result type assertions. */
47
+ declare function useWorkflow<W extends SchematizedWorkflow>(args: {
48
+ id: string;
49
+ workflow: W;
50
+ input: z.input<W["inputSchema"]>;
51
+ }): Promise<{
52
+ data: z.output<W["outputSchema"]>;
53
+ }>;
54
+ declare function useWorkflow(args: {
55
+ id: string;
56
+ workflow: string;
57
+ input: unknown;
58
+ }): Promise<{
59
+ data: Record<string, unknown>;
60
+ }>;
61
+
7
62
  type HookNodeRuntimeContext = {
8
63
  graph: {
9
64
  name: string;
@@ -12,6 +67,8 @@ type HookNodeRuntimeContext = {
12
67
  config: NodeConfig;
13
68
  emit: (event: string, payload: unknown) => void;
14
69
  awaitInterrupt: (args: InterruptInput) => InterruptResult;
70
+ callWorkflow?: WorkflowCallService;
71
+ workflowCallTelemetry?: KortyxTelemetryConfig | undefined;
15
72
  };
16
73
  type HookRuntimeContext = {
17
74
  node: HookNodeRuntimeContext;
@@ -161,4 +218,7 @@ declare function useRuntimeContext<TContext extends Record<string, unknown> = Re
161
218
  declare function useNodeState<T>(initialValue: T): [T, StateSetter<T>];
162
219
  declare function useWorkflowState<T>(key: string, ...rest: [] | [T]): [T, StateSetter<T>];
163
220
 
164
- export { type HookRuntimeContext, KortyxTraceMetadata, ReasonTraceAdapter, type UseInterruptArgs, type UseReasonArgs, type UseReasonResult, type UseReasonStep, type UseReasonToolExecution, type UseStructuredDataArgs, runWithHookContext, useInterrupt, useNodeState, useReason, useRuntimeContext, useStructuredData, useWorkflowState };
221
+ /** Best effort lifecycle facts. Engine snapshots and interrupt tokens never leave the runtime. */
222
+ declare function emitWorkflowCall(telemetry: KortyxTelemetryConfig | undefined, type: KortyxTelemetryEventType, payload: Record<string, unknown>): void;
223
+
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 };
package/dist/index.js CHANGED
@@ -19,13 +19,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // src/index.ts
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
+ WorkflowCallError: () => WorkflowCallError,
23
+ createWorkflowHooks: () => createWorkflowHooks,
24
+ emitWorkflowCall: () => emitWorkflowCall,
22
25
  runWithHookContext: () => runWithHookContext,
23
26
  useInterrupt: () => useInterrupt,
24
27
  useNodeState: () => useNodeState,
25
28
  useReason: () => useReason2,
26
29
  useRuntimeContext: () => useRuntimeContext,
27
30
  useStructuredData: () => useStructuredData,
28
- useWorkflowState: () => useWorkflowState
31
+ useWorkflow: () => useWorkflow,
32
+ useWorkflowState: () => useWorkflowState,
33
+ workflowCallFingerprint: () => workflowCallFingerprint
29
34
  });
30
35
  module.exports = __toCommonJS(index_exports);
31
36
 
@@ -78,6 +83,8 @@ var createInternalContext = (ctx) => {
78
83
  const currentNodeState = storedNodeId && storedNodeId === nodeId ? normalizeNodeState(storedState) : { byIndex: [], byKey: {} };
79
84
  return {
80
85
  ...ctx,
86
+ workflowCallActive: false,
87
+ workflowCallIds: /* @__PURE__ */ new Set(),
81
88
  nodeStateIndex: 0,
82
89
  reasonCallIndex: 0,
83
90
  currentNodeState,
@@ -151,6 +158,12 @@ async function runWithHookContext(ctx, fn) {
151
158
  try {
152
159
  const result = await storage.run(internal, fn);
153
160
  cleanupCompletedReasonCheckpoints(internal);
161
+ for (const key of Object.keys(internal.currentNodeState.byKey)) {
162
+ if (key.startsWith("__useWorkflow:") || key === "__useWorkflowActivation") {
163
+ internal.currentNodeState.byKey[key] = null;
164
+ internal.stateDirty = true;
165
+ }
166
+ }
154
167
  return { result, runtimeUpdates: buildRuntimeStateUpdates(internal) };
155
168
  } catch (err) {
156
169
  const runtimeUpdates = buildRuntimeStateUpdates(internal);
@@ -248,7 +261,10 @@ var awaitInterruptInternal = (args) => {
248
261
  ...typeof args.id === "string" && args.id.length > 0 ? { id: args.id } : {},
249
262
  meta: {
250
263
  ...userMeta,
251
- __kortyxResumeStatePatch: resumeStatePatch
264
+ __kortyxResumeStatePatch: {
265
+ ...resumeStatePatch,
266
+ ...ctx.tokenUsage ? { tokenUsage: ctx.tokenUsage } : {}
267
+ }
252
268
  }
253
269
  };
254
270
  const raw = ctx.node.awaitInterrupt(enrichedRequest);
@@ -2331,14 +2347,243 @@ function useWorkflowState(key, ...rest) {
2331
2347
  };
2332
2348
  return [getValue(), setValue];
2333
2349
  }
2350
+
2351
+ // src/workflow.ts
2352
+ var import_node_crypto5 = require("crypto");
2353
+
2354
+ // src/workflow-telemetry.ts
2355
+ var import_node_crypto4 = require("crypto");
2356
+ function emitWorkflowCall(telemetry, type, payload) {
2357
+ const base = telemetry?.correlation;
2358
+ if (!telemetry?.reporter || !telemetry.environment || !telemetry.service || !base?.runId || !base.workflowId)
2359
+ return;
2360
+ try {
2361
+ const active = telemetry.trace?.getActiveContext?.();
2362
+ void Promise.resolve(
2363
+ telemetry.reporter.emit([
2364
+ {
2365
+ schemaVersion: 1,
2366
+ eventId: (0, import_node_crypto4.randomUUID)(),
2367
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
2368
+ environment: telemetry.environment,
2369
+ service: telemetry.service,
2370
+ correlation: {
2371
+ ...base,
2372
+ runId: base.runId,
2373
+ workflowId: base.workflowId,
2374
+ ...active
2375
+ },
2376
+ type,
2377
+ payload
2378
+ }
2379
+ ])
2380
+ ).catch(() => {
2381
+ });
2382
+ } catch {
2383
+ }
2384
+ }
2385
+ function workflowCallContent(telemetry, side, value) {
2386
+ const policy = telemetry?.captureContent;
2387
+ if (!(policy === true || policy && typeof policy === "object" && policy[side]))
2388
+ return {};
2389
+ const serialized = JSON.stringify(value);
2390
+ return serialized && Buffer.byteLength(serialized, "utf8") <= 16384 ? { [side]: JSON.parse(serialized) } : { [`${side}Omitted`]: "Content exceeds 16 KiB" };
2391
+ }
2392
+
2393
+ // src/workflow.ts
2394
+ var WorkflowCallError = class extends Error {
2395
+ constructor(workflow, message) {
2396
+ super(`Workflow '${workflow}': ${message}`);
2397
+ this.workflow = workflow;
2398
+ this.name = "WorkflowCallError";
2399
+ }
2400
+ workflow;
2401
+ };
2402
+ function workflowCallFingerprint(value) {
2403
+ const seen = /* @__PURE__ */ new Set();
2404
+ const canonical = (v) => {
2405
+ if (v === null || typeof v === "string" || typeof v === "boolean") return v;
2406
+ if (typeof v === "number" && Number.isFinite(v)) return v;
2407
+ if (typeof v !== "object" || !v || seen.has(v)) {
2408
+ throw new Error("Workflow input and output must be JSON serializable.");
2409
+ }
2410
+ if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype) {
2411
+ throw new Error(
2412
+ "Workflow input and output must contain plain JSON objects."
2413
+ );
2414
+ }
2415
+ seen.add(v);
2416
+ const result = Array.isArray(v) ? v.map(canonical) : Object.fromEntries(
2417
+ Object.keys(v).sort().map((key) => [
2418
+ key,
2419
+ canonical(v[key])
2420
+ ])
2421
+ );
2422
+ seen.delete(v);
2423
+ return result;
2424
+ };
2425
+ return JSON.stringify(canonical(value));
2426
+ }
2427
+ function createWorkflowHooks(workflows) {
2428
+ return {
2429
+ useWorkflow: (args) => {
2430
+ const workflow = workflows[args.workflow];
2431
+ if (!workflow || workflow.id !== args.workflow)
2432
+ throw new Error("Workflow registry keys must match definition ids.");
2433
+ return useWorkflow({ ...args, workflow });
2434
+ }
2435
+ };
2436
+ }
2437
+ async function useWorkflow(args) {
2438
+ const ctx = getHookContext();
2439
+ const service = ctx.node.callWorkflow;
2440
+ if (!service)
2441
+ throw new Error("useWorkflow requires an agent workflow registry.");
2442
+ if (!args.id.trim())
2443
+ throw new Error("useWorkflow requires a stable, nonempty id.");
2444
+ if (ctx.workflowCallActive)
2445
+ throw new Error(
2446
+ "Concurrent workflow calls are not supported; await each call."
2447
+ );
2448
+ if (ctx.workflowCallIds.has(args.id))
2449
+ throw new Error(
2450
+ `Duplicate workflow call id '${args.id}' in one node activation.`
2451
+ );
2452
+ if (ctx.workflowCallIds.size >= 64)
2453
+ throw new Error(
2454
+ "A node may call at most 64 child workflows per activation."
2455
+ );
2456
+ ctx.workflowCallIds.add(args.id);
2457
+ const definition = typeof args.workflow === "string" ? void 0 : args.workflow;
2458
+ const workflow = definition?.id ?? args.workflow;
2459
+ const input = args.input;
2460
+ const fingerprint = workflowCallFingerprint({
2461
+ workflow,
2462
+ input,
2463
+ version: definition?.version ?? null
2464
+ });
2465
+ const key = `__useWorkflow:${args.id}`;
2466
+ let record = ctx.currentNodeState.byKey[key];
2467
+ if (record && record.fingerprint !== fingerprint)
2468
+ throw new WorkflowCallError(
2469
+ workflow,
2470
+ "call input or target changed during replay"
2471
+ );
2472
+ record ??= {
2473
+ fingerprint,
2474
+ invocationId: (0, import_node_crypto5.randomUUID)(),
2475
+ status: "running",
2476
+ interrupts: []
2477
+ };
2478
+ ctx.currentNodeState.byKey[key] = record;
2479
+ ctx.stateDirty = true;
2480
+ ctx.workflowCallActive = true;
2481
+ const telemetry = ctx.node.workflowCallTelemetry;
2482
+ const activationKey = "__useWorkflowActivation";
2483
+ ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto5.randomUUID)();
2484
+ const callerNodeExecutionId = ctx.currentNodeState.byKey[activationKey];
2485
+ const metadata = {
2486
+ invocationId: record.invocationId,
2487
+ parentInvocationId: telemetry?.correlation?.invocationId ?? null,
2488
+ branchId: telemetry?.correlation?.branchId ?? telemetry?.correlation?.runId ?? "unknown",
2489
+ callId: args.id,
2490
+ callerNodeId: ctx.node.graph.node,
2491
+ callerNodeExecutionId,
2492
+ sourceWorkflowId: ctx.node.graph.name,
2493
+ targetWorkflowId: workflow,
2494
+ targetVersion: definition?.version ?? null
2495
+ };
2496
+ record.telemetry = metadata;
2497
+ const report = (status, content = {}) => {
2498
+ record.sequence = (record.sequence ?? 0) + 1;
2499
+ emitWorkflowCall(telemetry, `workflow.call.${status}`, {
2500
+ ...metadata,
2501
+ sequence: record.sequence,
2502
+ ...content
2503
+ });
2504
+ };
2505
+ const cached = record.status === "completed";
2506
+ try {
2507
+ for (const interruption of record.interrupts) {
2508
+ interruption.response = await awaitInterruptInternal({
2509
+ request: interruption.request,
2510
+ ...interruption.request.meta ? { meta: interruption.request.meta } : {}
2511
+ });
2512
+ }
2513
+ if (record.status === "failed")
2514
+ throw new WorkflowCallError(workflow, record.error ?? "child failed");
2515
+ while (record.status !== "completed") {
2516
+ let outcome;
2517
+ try {
2518
+ report(
2519
+ record.snapshot ? "resumed" : "started",
2520
+ workflowCallContent(telemetry, "input", input)
2521
+ );
2522
+ outcome = await service({
2523
+ callerNodeExecutionId,
2524
+ id: args.id,
2525
+ workflow,
2526
+ input,
2527
+ definition,
2528
+ invocationId: record.invocationId,
2529
+ ...record.snapshot ? { snapshot: record.snapshot } : {},
2530
+ ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
2531
+ });
2532
+ } catch (error) {
2533
+ record.status = "failed";
2534
+ record.error = error instanceof Error ? error.message : String(error);
2535
+ report("failed", { error: "Child workflow failed" });
2536
+ throw new WorkflowCallError(workflow, record.error);
2537
+ }
2538
+ if (outcome.status === "completed") {
2539
+ record.data = outcome.data;
2540
+ accumulateTokenUsage(outcome.usage);
2541
+ workflowCallFingerprint(record.data);
2542
+ record.status = "completed";
2543
+ delete record.snapshot;
2544
+ report(
2545
+ "completed",
2546
+ workflowCallContent(telemetry, "output", record.data)
2547
+ );
2548
+ } else {
2549
+ record.status = "interrupted";
2550
+ record.snapshot = outcome.snapshot;
2551
+ report("suspended", {
2552
+ leaf: outcome.request.meta?.workflowCall ?? null
2553
+ });
2554
+ const interruption = {
2555
+ request: outcome.request
2556
+ };
2557
+ record.interrupts.push(interruption);
2558
+ interruption.response = await awaitInterruptInternal({
2559
+ request: interruption.request,
2560
+ ...interruption.request.meta ? { meta: interruption.request.meta } : {}
2561
+ });
2562
+ }
2563
+ }
2564
+ if (cached)
2565
+ report("reused", workflowCallContent(telemetry, "output", record.data));
2566
+ const data = record.data ?? {};
2567
+ return {
2568
+ data: JSON.parse(JSON.stringify(data))
2569
+ };
2570
+ } finally {
2571
+ ctx.workflowCallActive = false;
2572
+ }
2573
+ }
2334
2574
  // Annotate the CommonJS export names for ESM import in node:
2335
2575
  0 && (module.exports = {
2576
+ WorkflowCallError,
2577
+ createWorkflowHooks,
2578
+ emitWorkflowCall,
2336
2579
  runWithHookContext,
2337
2580
  useInterrupt,
2338
2581
  useNodeState,
2339
2582
  useReason,
2340
2583
  useRuntimeContext,
2341
2584
  useStructuredData,
2342
- useWorkflowState
2585
+ useWorkflow,
2586
+ useWorkflowState,
2587
+ workflowCallFingerprint
2343
2588
  });
2344
2589
  //# sourceMappingURL=index.js.map