@kortyx/hooks 0.24.0 → 0.25.1

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/dist/index.js CHANGED
@@ -289,13 +289,18 @@ var resolveHookStatePatch = (args) => ({
289
289
  });
290
290
 
291
291
  // src/validation.ts
292
+ var import_errors = require("@kortyx/core/errors");
292
293
  var parseWithSchema = (schema, value, label) => {
293
294
  if (!schema) return value;
294
295
  const parsed = schema.safeParse(value);
295
296
  if (parsed.success) return parsed.data;
296
297
  const parsedError = "error" in parsed && parsed.error ? parsed.error : void 0;
297
298
  const reason = typeof parsedError?.message === "string" && parsedError.message.length > 0 ? parsedError.message : "invalid payload";
298
- throw new Error(`${label} validation failed: ${reason}`);
299
+ throw new import_errors.ValidationError(
300
+ label === "useReason output" ? "MODEL_OUTPUT_SCHEMA" : "SCHEMA_VALIDATION",
301
+ `${label} validation failed: ${reason}`,
302
+ parsedError
303
+ );
299
304
  };
300
305
 
301
306
  // src/interrupt.ts
@@ -437,6 +442,21 @@ var import_core3 = require("@kortyx/core");
437
442
  // src/reason-engine.ts
438
443
  var import_node_crypto2 = require("crypto");
439
444
  var import_core2 = require("@kortyx/core");
445
+ var import_errors2 = require("@kortyx/core/errors");
446
+ function assertModelFinish(result) {
447
+ if (result.finishReason?.unified !== "content-filter") return;
448
+ throw Object.assign(
449
+ new import_errors2.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
450
+ category: "provider",
451
+ retryable: false,
452
+ safeMessage: "The provider refused the request."
453
+ }),
454
+ {
455
+ ...result.usage ? { usage: result.usage } : {},
456
+ finishReason: result.finishReason
457
+ }
458
+ );
459
+ }
440
460
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
441
461
  var createRuntimeId = () => {
442
462
  try {
@@ -550,15 +570,18 @@ async function runReasonEngine(args) {
550
570
  };
551
571
  const failTrace = (span, error) => {
552
572
  const detail = error;
553
- span?.fail?.(error, {
554
- ...detail?.usage ? { usage: detail.usage } : {},
555
- ...detail?.finishReason ? { finishReason: detail.finishReason } : {},
556
- ...detail?.providerMetadata ? { providerMetadata: detail.providerMetadata } : {},
557
- attributes: {
558
- providerId: args.model.provider.id,
559
- modelId: args.model.modelId
560
- }
561
- });
573
+ try {
574
+ span?.fail?.(error, {
575
+ ...detail?.usage ? { usage: detail.usage } : {},
576
+ ...detail?.finishReason ? { finishReason: detail.finishReason } : {},
577
+ ...detail?.providerMetadata ? { providerMetadata: detail.providerMetadata } : {},
578
+ attributes: {
579
+ providerId: args.model.provider.id,
580
+ modelId: args.model.modelId
581
+ }
582
+ });
583
+ } catch {
584
+ }
562
585
  };
563
586
  try {
564
587
  if (stream) {
@@ -643,6 +666,8 @@ async function runReasonEngine(args) {
643
666
  }
644
667
  }
645
668
  }
669
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
670
+ assertModelFinish({ finishReason, usage });
646
671
  if (emit) {
647
672
  emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
648
673
  ...commonMeta
@@ -664,6 +689,7 @@ async function runReasonEngine(args) {
664
689
  }
665
690
  const response = await model.invoke(messages);
666
691
  (0, import_core2.throwIfExecutionAborted)(abortSignal);
692
+ assertModelFinish(response);
667
693
  if (response.content) args.onTextChunk?.(response.content);
668
694
  if (emit) {
669
695
  emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
@@ -1463,6 +1489,7 @@ function createStructuredOutputStreamer(args, id, opId) {
1463
1489
  }
1464
1490
 
1465
1491
  // src/reason/parsing.ts
1492
+ var import_errors3 = require("@kortyx/core/errors");
1466
1493
  var tryJsonParse = (value) => {
1467
1494
  try {
1468
1495
  return { ok: true, value: JSON.parse(value) };
@@ -1500,132 +1527,135 @@ var resolveOutputCandidate = (text) => {
1500
1527
  }
1501
1528
  return text;
1502
1529
  };
1503
- var looksLikeStructuredJsonAttempt = (value) => {
1504
- const trimmed = value.trim();
1505
- if (!trimmed) return false;
1506
- return trimmed.startsWith("{") || trimmed.startsWith("[") || /^```(?:json)?/i.test(trimmed);
1507
- };
1508
- var isLikelyTruncatedStructuredOutput = (text) => {
1509
- const trimmed = text.trim();
1510
- if (!looksLikeStructuredJsonAttempt(trimmed)) return false;
1511
- const direct = tryJsonParse(trimmed);
1512
- if (!direct.ok && direct.error) return true;
1513
- const block = extractJsonCodeBlock(trimmed);
1514
- if (!block) return false;
1515
- const parsedBlock = tryJsonParse(block);
1516
- return !parsedBlock.ok && Boolean(parsedBlock.error);
1517
- };
1518
- var createTruncatedStructuredOutputError = (label) => new Error(
1519
- `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`
1530
+ var createTruncatedStructuredOutputError = (label, cause) => new import_errors3.ValidationError(
1531
+ "OUTPUT_TRUNCATED",
1532
+ `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`,
1533
+ cause
1520
1534
  );
1521
- var createInvalidStructuredOutputError = (label) => new Error(
1522
- `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. This often happens when output is truncated or the provider does not follow JSON mode. Increase maxOutputTokens or simplify the requested output.`
1535
+ var createInvalidStructuredOutputError = (label, cause) => new import_errors3.ValidationError(
1536
+ "INVALID_MODEL_JSON",
1537
+ `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. Check outputSchema and provider structured-output settings.`,
1538
+ cause
1523
1539
  );
1524
1540
  var parseReasonOutputWithSchema = (args) => {
1525
1541
  const candidate = resolveOutputCandidate(args.text);
1542
+ const block = extractJsonCodeBlock(args.text.trim());
1543
+ const validJson = tryJsonParse(args.text.trim()).ok || block !== null && tryJsonParse(block).ok;
1526
1544
  try {
1527
1545
  return parseWithSchema(args.schema, candidate, args.label);
1528
1546
  } catch (error) {
1529
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1530
- throw createTruncatedStructuredOutputError(args.label);
1531
- }
1532
- if (typeof candidate === "string" && candidate.trim().length > 0) {
1533
- throw createInvalidStructuredOutputError(args.label);
1534
- }
1535
- throw error;
1547
+ if (!(error instanceof import_errors3.ValidationError)) throw error;
1548
+ const failure = args.finishReason?.unified === "length" ? createTruncatedStructuredOutputError(args.label, error) : typeof candidate === "string" && !validJson ? createInvalidStructuredOutputError(args.label, error) : error;
1549
+ if (failure && typeof failure === "object")
1550
+ Object.assign(failure, {
1551
+ ...args.usage ? { usage: args.usage } : {},
1552
+ ...args.finishReason ? { finishReason: args.finishReason } : {}
1553
+ });
1554
+ throw failure;
1536
1555
  }
1537
1556
  };
1538
1557
  var parseInterruptFirstPassResult = (args) => {
1539
- const candidate = resolveOutputCandidate(args.text);
1540
- if (!isRecord2(candidate)) {
1541
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1542
- throw createTruncatedStructuredOutputError("useReason output");
1543
- }
1544
- if (typeof candidate === "string" && candidate.trim().length > 0) {
1545
- throw createInvalidStructuredOutputError("useReason output");
1546
- }
1547
- throw new Error(
1548
- "useReason first pass with interrupt must return a JSON object."
1549
- );
1550
- }
1551
- const mode = args.mode ?? "required";
1552
- let interruptRequired = true;
1553
- if (mode === "optional") {
1554
- if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
1555
- throw new Error(
1556
- 'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
1558
+ try {
1559
+ const candidate = resolveOutputCandidate(args.text);
1560
+ if (!isRecord2(candidate)) {
1561
+ if (args.finishReason?.unified === "length") {
1562
+ throw createTruncatedStructuredOutputError("useReason output");
1563
+ }
1564
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
1565
+ throw createInvalidStructuredOutputError("useReason output");
1566
+ }
1567
+ throw new import_errors3.ValidationError(
1568
+ "MODEL_OUTPUT_SCHEMA",
1569
+ "useReason first pass with interrupt must return a JSON object."
1557
1570
  );
1558
1571
  }
1559
- interruptRequired = candidate.decision === "interrupt";
1560
- if (!interruptRequired) {
1561
- const hasUnexpectedRequest = [
1572
+ const mode = args.mode ?? "required";
1573
+ let interruptRequired = true;
1574
+ if (mode === "optional") {
1575
+ if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
1576
+ throw new import_errors3.ValidationError(
1577
+ "MODEL_OUTPUT_SCHEMA",
1578
+ 'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
1579
+ );
1580
+ }
1581
+ interruptRequired = candidate.decision === "interrupt";
1582
+ if (!interruptRequired) {
1583
+ const hasUnexpectedRequest = [
1584
+ candidate.interruptRequest,
1585
+ candidate.request,
1586
+ candidate.interrupt
1587
+ ].some((value) => value !== null && value !== void 0);
1588
+ if (hasUnexpectedRequest) {
1589
+ throw new import_errors3.ValidationError(
1590
+ "MODEL_OUTPUT_SCHEMA",
1591
+ 'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
1592
+ );
1593
+ }
1594
+ }
1595
+ }
1596
+ let request;
1597
+ if (interruptRequired) {
1598
+ const requestCandidates = [
1562
1599
  candidate.interruptRequest,
1563
1600
  candidate.request,
1564
- candidate.interrupt
1565
- ].some((value) => value !== null && value !== void 0);
1566
- if (hasUnexpectedRequest) {
1567
- throw new Error(
1568
- 'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
1601
+ candidate.interrupt,
1602
+ mode === "required" ? candidate : void 0
1603
+ ];
1604
+ const requestPayload = requestCandidates.find(
1605
+ (value) => value !== null && value !== void 0
1606
+ );
1607
+ request = parseWithSchema(
1608
+ args.requestSchema,
1609
+ requestPayload,
1610
+ "useReason interrupt.request"
1611
+ );
1612
+ }
1613
+ let output;
1614
+ if (args.outputSchema) {
1615
+ const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
1616
+ try {
1617
+ output = parseWithSchema(
1618
+ args.outputSchema,
1619
+ outputPayload,
1620
+ "useReason output"
1569
1621
  );
1622
+ } catch (error) {
1623
+ if (!(error instanceof import_errors3.ValidationError)) throw error;
1624
+ if (args.finishReason?.unified === "length") {
1625
+ throw createTruncatedStructuredOutputError("useReason output", error);
1626
+ }
1627
+ throw error;
1570
1628
  }
1571
1629
  }
1572
- }
1573
- let request;
1574
- if (interruptRequired) {
1575
- const requestCandidates = [
1576
- candidate.interruptRequest,
1577
- candidate.request,
1578
- candidate.interrupt,
1579
- mode === "required" ? candidate : void 0
1580
- ];
1581
- const requestPayload = requestCandidates.find(
1582
- (value) => value !== null && value !== void 0
1583
- );
1584
- request = parseWithSchema(
1585
- args.requestSchema,
1586
- requestPayload,
1587
- "useReason interrupt.request"
1588
- );
1589
- }
1590
- let output;
1591
- if (args.outputSchema) {
1592
- const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
1593
- try {
1594
- output = parseWithSchema(
1595
- args.outputSchema,
1596
- outputPayload,
1597
- "useReason output"
1630
+ const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
1631
+ if (!args.outputSchema && !explicitDraft) {
1632
+ throw new import_errors3.ValidationError(
1633
+ "MODEL_OUTPUT_SCHEMA",
1634
+ "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
1598
1635
  );
1599
- } catch (error) {
1600
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1601
- throw createTruncatedStructuredOutputError("useReason output");
1602
- }
1603
- if (typeof outputPayload === "string" && outputPayload.trim().length > 0) {
1604
- throw createInvalidStructuredOutputError("useReason output");
1605
- }
1606
- throw error;
1607
1636
  }
1608
- }
1609
- const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
1610
- if (!args.outputSchema && !explicitDraft) {
1611
- throw new Error(
1612
- "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
1613
- );
1614
- }
1615
- const draftText = explicitDraft ?? (output !== void 0 ? tryStringify(output) : String(args.text ?? ""));
1616
- if (!interruptRequired) {
1637
+ const draftText = explicitDraft ?? (output !== void 0 ? tryStringify(output) : String(args.text ?? ""));
1638
+ if (!interruptRequired) {
1639
+ return {
1640
+ draftText,
1641
+ interruptRequired: false,
1642
+ ...output !== void 0 ? { output } : {}
1643
+ };
1644
+ }
1617
1645
  return {
1618
1646
  draftText,
1619
- interruptRequired: false,
1647
+ interruptRequired: true,
1648
+ request,
1620
1649
  ...output !== void 0 ? { output } : {}
1621
1650
  };
1651
+ } catch (error) {
1652
+ if (error && typeof error === "object")
1653
+ Object.assign(error, {
1654
+ ...args.usage ? { usage: args.usage } : {},
1655
+ ...args.finishReason ? { finishReason: args.finishReason } : {}
1656
+ });
1657
+ throw error;
1622
1658
  }
1623
- return {
1624
- draftText,
1625
- interruptRequired: true,
1626
- request,
1627
- ...output !== void 0 ? { output } : {}
1628
- };
1629
1659
  };
1630
1660
 
1631
1661
  // src/reason/prompting.ts
@@ -1839,7 +1869,7 @@ var emitReasonStructuredOutput = (args) => {
1839
1869
 
1840
1870
  // src/reason/tools.ts
1841
1871
  var import_core4 = require("@kortyx/core");
1842
- var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
1872
+ var import_errors4 = require("@kortyx/core/errors");
1843
1873
  var toToolDefinitions = (tools) => tools.map((tool) => ({
1844
1874
  name: tool.name,
1845
1875
  ...tool.title ? { title: tool.title } : {},
@@ -1873,17 +1903,27 @@ var normalizeToolResult = (toolCall, value) => {
1873
1903
  ...value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value) ? { structuredContent: value } : {}
1874
1904
  };
1875
1905
  };
1876
- var closeOwnedTools = async (tools) => {
1906
+ var closeOwnedTools = async (tools, preserveFailure = false, traceSpan) => {
1877
1907
  const closeFns = /* @__PURE__ */ new Set();
1878
1908
  for (const tool of tools) {
1879
1909
  if (tool.closeAfterUse === false || !tool.close) continue;
1880
1910
  closeFns.add(tool.close);
1881
1911
  }
1882
- await Promise.all(
1883
- [...closeFns].map(async (close) => {
1884
- await close();
1885
- })
1886
- );
1912
+ try {
1913
+ await Promise.all(
1914
+ [...closeFns].map(async (close) => {
1915
+ await close();
1916
+ })
1917
+ );
1918
+ } catch (error) {
1919
+ if (!preserveFailure) throw error;
1920
+ try {
1921
+ traceSpan?.addEvent?.("useReason.cleanup.failed", {
1922
+ failure: (0, import_errors4.serializeFailure)(error)
1923
+ });
1924
+ } catch {
1925
+ }
1926
+ }
1887
1927
  };
1888
1928
  var createInitialMessages = (args) => [
1889
1929
  ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
@@ -1900,6 +1940,7 @@ var runReasonToolLoop = async (args) => {
1900
1940
  const saved = ctx.currentNodeState.byKey[checkpointKey];
1901
1941
  const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
1902
1942
  const opId = checkpoint?.opId ?? args.opId;
1943
+ let primaryFailure = false;
1903
1944
  const tools = useReasonArgs.tools ?? [];
1904
1945
  const toolByName = /* @__PURE__ */ new Map();
1905
1946
  let validationCompleted = false;
@@ -1932,7 +1973,7 @@ var runReasonToolLoop = async (args) => {
1932
1973
  validationCompleted = true;
1933
1974
  } finally {
1934
1975
  if (!validationCompleted) {
1935
- await closeOwnedTools(tools);
1976
+ await closeOwnedTools(tools, true, traceSpan);
1936
1977
  }
1937
1978
  }
1938
1979
  const toolDefinitions = toToolDefinitions(tools);
@@ -2051,6 +2092,7 @@ var runReasonToolLoop = async (args) => {
2051
2092
  text: step.text,
2052
2093
  schema: useReasonArgs.outputSchema,
2053
2094
  ...step.finishReason ? { finishReason: step.finishReason } : {},
2095
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2054
2096
  label: "useReason output"
2055
2097
  });
2056
2098
  aggregatedWarnings = mergeWarnings(aggregatedWarnings, [
@@ -2138,12 +2180,12 @@ var runReasonToolLoop = async (args) => {
2138
2180
  options: [
2139
2181
  { id: "approve", label: "Approve" },
2140
2182
  { id: "deny", label: "Deny" }
2141
- ],
2142
- meta: {
2143
- tool: tool.name,
2144
- toolCallId: toolCall.id,
2145
- input: toolCall.input
2146
- }
2183
+ ]
2184
+ },
2185
+ meta: {
2186
+ tool: tool.name,
2187
+ toolCallId: toolCall.id,
2188
+ input: toolCall.input
2147
2189
  }
2148
2190
  });
2149
2191
  const approved = Array.isArray(approval) ? approval.includes("approve") : approval === "approve";
@@ -2205,12 +2247,12 @@ var runReasonToolLoop = async (args) => {
2205
2247
  });
2206
2248
  } catch (error) {
2207
2249
  (0, import_core4.throwIfExecutionAborted)(abortSignal);
2208
- if ((0, import_core4.isExecutionLimitReached)(error) || (0, import_core4.isExecutionCancelled)(error))
2209
- throw error;
2250
+ if ((0, import_errors4.isControlFlowError)(error)) throw error;
2210
2251
  const result2 = {
2211
2252
  toolCallId: toolCall.id,
2212
2253
  name: tool.name,
2213
- content: toErrorMessage(error),
2254
+ content: (0, import_errors4.serializeFailure)(error).message,
2255
+ failure: (0, import_errors4.serializeFailure)(error),
2214
2256
  isError: true
2215
2257
  };
2216
2258
  toolResults.push(result2);
@@ -2219,14 +2261,16 @@ var runReasonToolLoop = async (args) => {
2219
2261
  emitToolEvent("tool-call-error", {
2220
2262
  tool: tool.name,
2221
2263
  toolCallId: toolCall.id,
2222
- message: result2.content
2264
+ message: result2.content,
2265
+ failure: result2.failure
2223
2266
  });
2224
2267
  }
2225
2268
  traceSpan?.addEvent?.("useReason.tool-call.error", {
2226
2269
  stepIndex,
2227
2270
  tool: tool.name,
2228
2271
  toolCallId: toolCall.id,
2229
- message: result2.content
2272
+ message: result2.content,
2273
+ failure: result2.failure
2230
2274
  });
2231
2275
  }
2232
2276
  const completed2 = toolResults.at(-1);
@@ -2253,6 +2297,7 @@ var runReasonToolLoop = async (args) => {
2253
2297
  text: finalText,
2254
2298
  schema: useReasonArgs.outputSchema,
2255
2299
  ...finalFinishReason ? { finishReason: finalFinishReason } : {},
2300
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2256
2301
  label: "useReason output"
2257
2302
  });
2258
2303
  }
@@ -2299,15 +2344,19 @@ var runReasonToolLoop = async (args) => {
2299
2344
  ctx.stateDirty = true;
2300
2345
  return result;
2301
2346
  } catch (error) {
2302
- traceSpan?.fail?.(error, {
2303
- attributes: {
2304
- toolStepCount: steps.length,
2305
- toolCallCount: allToolCalls.length
2306
- }
2307
- });
2347
+ primaryFailure = true;
2348
+ try {
2349
+ traceSpan?.fail?.(error, {
2350
+ attributes: {
2351
+ toolStepCount: steps.length,
2352
+ toolCallCount: allToolCalls.length
2353
+ }
2354
+ });
2355
+ } catch {
2356
+ }
2308
2357
  throw error;
2309
2358
  } finally {
2310
- await closeOwnedTools(tools);
2359
+ await closeOwnedTools(tools, primaryFailure, traceSpan);
2311
2360
  }
2312
2361
  };
2313
2362
 
@@ -2472,6 +2521,7 @@ async function useReason(args) {
2472
2521
  text: first.text,
2473
2522
  requestSchema: args.interrupt.requestSchema,
2474
2523
  ...first.finishReason ? { finishReason: first.finishReason } : {},
2524
+ ...first.usage ? { usage: first.usage } : {},
2475
2525
  ...args.outputSchema ? { outputSchema: args.outputSchema } : {},
2476
2526
  mode: args.interrupt.mode ?? "required"
2477
2527
  });
@@ -2489,6 +2539,7 @@ async function useReason(args) {
2489
2539
  text: first.text,
2490
2540
  schema: args.outputSchema,
2491
2541
  ...first.finishReason ? { finishReason: first.finishReason } : {},
2542
+ ...first.usage ? { usage: first.usage } : {},
2492
2543
  label: "useReason output"
2493
2544
  });
2494
2545
  }
@@ -2583,6 +2634,7 @@ async function useReason(args) {
2583
2634
  text: finalText,
2584
2635
  schema: args.outputSchema,
2585
2636
  ...second.finishReason ? { finishReason: second.finishReason } : {},
2637
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2586
2638
  label: "useReason output"
2587
2639
  });
2588
2640
  }
@@ -2690,7 +2742,9 @@ function useWorkflowState(key, ...rest) {
2690
2742
 
2691
2743
  // src/parallel.ts
2692
2744
  var import_core6 = require("@kortyx/core");
2745
+ var import_errors5 = require("@kortyx/core/errors");
2693
2746
  var ParallelChildWaiting = class extends Error {
2747
+ name = "ParallelChildWaiting";
2694
2748
  };
2695
2749
  var ParallelError = class extends AggregateError {
2696
2750
  constructor(results) {
@@ -2702,8 +2756,23 @@ var ParallelError = class extends AggregateError {
2702
2756
  );
2703
2757
  this.results = results;
2704
2758
  this.name = "ParallelError";
2759
+ this.failure = (0, import_errors5.serializeFailure)({
2760
+ version: 1,
2761
+ code: this.code,
2762
+ category: "workflow",
2763
+ message: this.message,
2764
+ retryable: null,
2765
+ results: results.map(
2766
+ (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors5.serializeFailure)(result.reason) }
2767
+ )
2768
+ });
2705
2769
  }
2706
2770
  results;
2771
+ code = "PARALLEL_FAILED";
2772
+ failure;
2773
+ toJSON() {
2774
+ return (0, import_errors5.serializeFailure)(this);
2775
+ }
2707
2776
  };
2708
2777
  function registerWorkflowTask(id) {
2709
2778
  const ctx = getHookContext();
@@ -2843,6 +2912,7 @@ function parallel(values) {
2843
2912
  // src/workflow.ts
2844
2913
  var import_node_crypto5 = require("crypto");
2845
2914
  var import_core7 = require("@kortyx/core");
2915
+ var import_errors6 = require("@kortyx/core/errors");
2846
2916
 
2847
2917
  // src/workflow-telemetry.ts
2848
2918
  var import_node_crypto4 = require("crypto");
@@ -2884,10 +2954,20 @@ function workflowCallContent(telemetry, side, value) {
2884
2954
  }
2885
2955
 
2886
2956
  // src/workflow.ts
2887
- var WorkflowCallError = class extends Error {
2888
- constructor(workflow, message) {
2889
- super(`Workflow '${workflow}': ${message}`);
2957
+ var WorkflowCallError = class extends import_errors6.KortyxError {
2958
+ constructor(workflow, message, cause, failure) {
2959
+ const detail = (0, import_errors6.serializeFailure)(
2960
+ failure ?? cause ?? new import_errors6.WorkflowContractError(message)
2961
+ );
2962
+ super(detail.code, `Workflow '${workflow}': ${message}`, {
2963
+ ...detail,
2964
+ safeMessage: detail.message,
2965
+ ...cause !== void 0 ? { cause } : {}
2966
+ });
2890
2967
  this.workflow = workflow;
2968
+ Object.assign(this.failure, detail, {
2969
+ context: { ...detail.context, workflow }
2970
+ });
2891
2971
  this.name = "WorkflowCallError";
2892
2972
  }
2893
2973
  workflow;
@@ -2898,10 +2978,12 @@ function workflowCallFingerprint(value) {
2898
2978
  if (v === null || typeof v === "string" || typeof v === "boolean") return v;
2899
2979
  if (typeof v === "number" && Number.isFinite(v)) return v;
2900
2980
  if (typeof v !== "object" || !v || seen.has(v)) {
2901
- throw new Error("Workflow input and output must be JSON serializable.");
2981
+ throw new import_errors6.WorkflowContractError(
2982
+ "Workflow input and output must be JSON serializable."
2983
+ );
2902
2984
  }
2903
2985
  if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype) {
2904
- throw new Error(
2986
+ throw new import_errors6.WorkflowContractError(
2905
2987
  "Workflow input and output must contain plain JSON objects."
2906
2988
  );
2907
2989
  }
@@ -2922,7 +3004,9 @@ function createWorkflowHooks(workflows) {
2922
3004
  useWorkflow: (args) => {
2923
3005
  const workflow = workflows[args.workflow];
2924
3006
  if (!workflow || workflow.id !== args.workflow)
2925
- throw new Error("Workflow registry keys must match definition ids.");
3007
+ throw new import_errors6.WorkflowContractError(
3008
+ "Workflow registry keys must match definition ids."
3009
+ );
2926
3010
  return useWorkflow({ ...args, workflow });
2927
3011
  }
2928
3012
  };
@@ -2946,19 +3030,23 @@ async function runWorkflowCall(args, task) {
2946
3030
  (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
2947
3031
  const service = ctx.node.callWorkflow;
2948
3032
  if (!service)
2949
- throw new Error("useWorkflow requires an agent workflow registry.");
3033
+ throw new import_errors6.WorkflowContractError(
3034
+ "useWorkflow requires an agent workflow registry."
3035
+ );
2950
3036
  if (!args.id.trim())
2951
- throw new Error("useWorkflow requires a stable, nonempty id.");
3037
+ throw new import_errors6.WorkflowContractError(
3038
+ "useWorkflow requires a stable, nonempty id."
3039
+ );
2952
3040
  if (ctx.workflowCallActive && !task.group)
2953
- throw new Error(
3041
+ throw new import_errors6.WorkflowContractError(
2954
3042
  "Concurrent workflow calls require parallel([...]); otherwise await each call."
2955
3043
  );
2956
3044
  if (ctx.workflowCallIds.has(args.id))
2957
- throw new Error(
3045
+ throw new import_errors6.WorkflowContractError(
2958
3046
  `Duplicate workflow call id '${args.id}' in one node activation.`
2959
3047
  );
2960
3048
  if (ctx.workflowCallIds.size >= 64)
2961
- throw new Error(
3049
+ throw new import_errors6.WorkflowContractError(
2962
3050
  "A node may call at most 64 child workflows per activation."
2963
3051
  );
2964
3052
  ctx.workflowCallIds.add(args.id);
@@ -3035,7 +3123,12 @@ async function runWorkflowCall(args, task) {
3035
3123
  });
3036
3124
  }
3037
3125
  if (record.status === "failed")
3038
- throw new WorkflowCallError(workflow, record.error ?? "child failed");
3126
+ throw new WorkflowCallError(
3127
+ workflow,
3128
+ record.error ?? "child failed",
3129
+ void 0,
3130
+ record.failure ?? (0, import_errors6.serializeFailure)(void 0)
3131
+ );
3039
3132
  if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
3040
3133
  throw new ParallelChildWaiting();
3041
3134
  while (record.status !== "completed") {
@@ -3070,9 +3163,15 @@ async function runWorkflowCall(args, task) {
3070
3163
  throw error;
3071
3164
  }
3072
3165
  record.status = "failed";
3073
- record.error = error instanceof Error ? error.message : String(error);
3166
+ record.failure = (0, import_errors6.serializeFailure)(error);
3167
+ record.error = record.failure.message;
3074
3168
  report("failed", { error: "Child workflow failed" });
3075
- throw new WorkflowCallError(workflow, record.error);
3169
+ throw new WorkflowCallError(
3170
+ workflow,
3171
+ record.error,
3172
+ error,
3173
+ record.failure
3174
+ );
3076
3175
  }
3077
3176
  (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
3078
3177
  recordUsage(outcome.usage);