@kortyx/agent 0.20.1 → 0.22.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/dist/index.js CHANGED
@@ -51,10 +51,12 @@ var require_dist = __commonJS({
51
51
  var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
52
52
  var index_exports2 = {};
53
53
  __export2(index_exports2, {
54
+ ParallelError: () => ParallelError,
54
55
  WorkflowCallError: () => WorkflowCallError,
55
56
  completeResponse: () => completeResponse,
56
57
  createWorkflowHooks: () => createWorkflowHooks,
57
58
  emitWorkflowCall: () => emitWorkflowCall2,
59
+ parallel: () => parallel,
58
60
  runWithHookContext: () => runWithHookContext,
59
61
  useAbortSignal: () => useAbortSignal,
60
62
  useInterrupt: () => useInterrupt,
@@ -118,6 +120,11 @@ var require_dist = __commonJS({
118
120
  ...ctx,
119
121
  workflowCallActive: false,
120
122
  workflowCallIds: /* @__PURE__ */ new Set(),
123
+ workflowTasks: [],
124
+ parallelJoins: [],
125
+ parallelGroup: void 0,
126
+ parallelIndex: 0,
127
+ workflowContextClosed: false,
121
128
  nodeStateIndex: 0,
122
129
  reasonCallIndex: 0,
123
130
  currentNodeState,
@@ -189,10 +196,29 @@ var require_dist = __commonJS({
189
196
  async function runWithHookContext(ctx, fn) {
190
197
  const internal = createInternalContext(ctx);
191
198
  try {
192
- const result = await storage.run(internal, fn);
199
+ const result = await storage.run(internal, async () => {
200
+ const drain = async () => {
201
+ let drained = -1;
202
+ while (drained !== internal.parallelJoins.length + internal.workflowTasks.length) {
203
+ drained = internal.parallelJoins.length + internal.workflowTasks.length;
204
+ await Promise.allSettled(internal.parallelJoins);
205
+ await Promise.all(internal.workflowTasks.map((task) => task.done));
206
+ }
207
+ internal.workflowContextClosed = true;
208
+ };
209
+ try {
210
+ const result2 = await fn();
211
+ await drain();
212
+ if (internal.workflowControlError) throw internal.workflowControlError;
213
+ return result2;
214
+ } catch (error) {
215
+ await drain();
216
+ throw internal.workflowControlError ?? error;
217
+ }
218
+ });
193
219
  cleanupCompletedReasonCheckpoints(internal);
194
220
  for (const key of Object.keys(internal.currentNodeState.byKey)) {
195
- if (key.startsWith("__useWorkflow:") || key === "__useWorkflowActivation") {
221
+ if (key.startsWith("__useWorkflow:") || key.startsWith("__parallel:") || key === "__useWorkflowActivation") {
196
222
  internal.currentNodeState.byKey[key] = null;
197
223
  internal.stateDirty = true;
198
224
  }
@@ -283,13 +309,18 @@ var require_dist = __commonJS({
283
309
  workflowState: args.workflowState
284
310
  }
285
311
  });
312
+ var import_errors6 = require("@kortyx/core/errors");
286
313
  var parseWithSchema = (schema, value, label) => {
287
314
  if (!schema) return value;
288
315
  const parsed = schema.safeParse(value);
289
316
  if (parsed.success) return parsed.data;
290
317
  const parsedError = "error" in parsed && parsed.error ? parsed.error : void 0;
291
318
  const reason = typeof parsedError?.message === "string" && parsedError.message.length > 0 ? parsedError.message : "invalid payload";
292
- throw new Error(`${label} validation failed: ${reason}`);
319
+ throw new import_errors6.ValidationError(
320
+ label === "useReason output" ? "MODEL_OUTPUT_SCHEMA" : "SCHEMA_VALIDATION",
321
+ `${label} validation failed: ${reason}`,
322
+ parsedError
323
+ );
293
324
  };
294
325
  var awaitInterruptInternal = (args) => {
295
326
  const ctx = getHookContext();
@@ -417,6 +448,21 @@ var require_dist = __commonJS({
417
448
  var import_core32 = require("@kortyx/core");
418
449
  var import_node_crypto22 = require("crypto");
419
450
  var import_core22 = require("@kortyx/core");
451
+ var import_errors22 = require("@kortyx/core/errors");
452
+ function assertModelFinish(result) {
453
+ if (result.finishReason?.unified !== "content-filter") return;
454
+ throw Object.assign(
455
+ new import_errors22.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
456
+ category: "provider",
457
+ retryable: false,
458
+ safeMessage: "The provider refused the request."
459
+ }),
460
+ {
461
+ ...result.usage ? { usage: result.usage } : {},
462
+ finishReason: result.finishReason
463
+ }
464
+ );
465
+ }
420
466
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
421
467
  var createRuntimeId = () => {
422
468
  try {
@@ -530,15 +576,18 @@ var require_dist = __commonJS({
530
576
  };
531
577
  const failTrace = (span, error) => {
532
578
  const detail = error;
533
- span?.fail?.(error, {
534
- ...detail?.usage ? { usage: detail.usage } : {},
535
- ...detail?.finishReason ? { finishReason: detail.finishReason } : {},
536
- ...detail?.providerMetadata ? { providerMetadata: detail.providerMetadata } : {},
537
- attributes: {
538
- providerId: args.model.provider.id,
539
- modelId: args.model.modelId
540
- }
541
- });
579
+ try {
580
+ span?.fail?.(error, {
581
+ ...detail?.usage ? { usage: detail.usage } : {},
582
+ ...detail?.finishReason ? { finishReason: detail.finishReason } : {},
583
+ ...detail?.providerMetadata ? { providerMetadata: detail.providerMetadata } : {},
584
+ attributes: {
585
+ providerId: args.model.provider.id,
586
+ modelId: args.model.modelId
587
+ }
588
+ });
589
+ } catch {
590
+ }
542
591
  };
543
592
  try {
544
593
  if (stream) {
@@ -623,6 +672,8 @@ var require_dist = __commonJS({
623
672
  }
624
673
  }
625
674
  }
675
+ (0, import_core22.throwIfExecutionAborted)(abortSignal);
676
+ assertModelFinish({ finishReason, usage });
626
677
  if (emit) {
627
678
  emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
628
679
  ...commonMeta
@@ -644,6 +695,7 @@ var require_dist = __commonJS({
644
695
  }
645
696
  const response = await model.invoke(messages);
646
697
  (0, import_core22.throwIfExecutionAborted)(abortSignal);
698
+ assertModelFinish(response);
647
699
  if (response.content) args.onTextChunk?.(response.content);
648
700
  if (emit) {
649
701
  emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
@@ -1433,6 +1485,7 @@ var require_dist = __commonJS({
1433
1485
  }
1434
1486
  };
1435
1487
  }
1488
+ var import_errors32 = require("@kortyx/core/errors");
1436
1489
  var tryJsonParse = (value) => {
1437
1490
  try {
1438
1491
  return { ok: true, value: JSON.parse(value) };
@@ -1470,132 +1523,135 @@ var require_dist = __commonJS({
1470
1523
  }
1471
1524
  return text;
1472
1525
  };
1473
- var looksLikeStructuredJsonAttempt = (value) => {
1474
- const trimmed = value.trim();
1475
- if (!trimmed) return false;
1476
- return trimmed.startsWith("{") || trimmed.startsWith("[") || /^```(?:json)?/i.test(trimmed);
1477
- };
1478
- var isLikelyTruncatedStructuredOutput = (text) => {
1479
- const trimmed = text.trim();
1480
- if (!looksLikeStructuredJsonAttempt(trimmed)) return false;
1481
- const direct = tryJsonParse(trimmed);
1482
- if (!direct.ok && direct.error) return true;
1483
- const block = extractJsonCodeBlock(trimmed);
1484
- if (!block) return false;
1485
- const parsedBlock = tryJsonParse(block);
1486
- return !parsedBlock.ok && Boolean(parsedBlock.error);
1487
- };
1488
- var createTruncatedStructuredOutputError = (label) => new Error(
1489
- `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`
1526
+ var createTruncatedStructuredOutputError = (label, cause) => new import_errors32.ValidationError(
1527
+ "OUTPUT_TRUNCATED",
1528
+ `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`,
1529
+ cause
1490
1530
  );
1491
- var createInvalidStructuredOutputError = (label) => new Error(
1492
- `${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.`
1531
+ var createInvalidStructuredOutputError = (label, cause) => new import_errors32.ValidationError(
1532
+ "INVALID_MODEL_JSON",
1533
+ `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. Check outputSchema and provider structured-output settings.`,
1534
+ cause
1493
1535
  );
1494
1536
  var parseReasonOutputWithSchema = (args) => {
1495
1537
  const candidate = resolveOutputCandidate(args.text);
1538
+ const block = extractJsonCodeBlock(args.text.trim());
1539
+ const validJson = tryJsonParse(args.text.trim()).ok || block !== null && tryJsonParse(block).ok;
1496
1540
  try {
1497
1541
  return parseWithSchema(args.schema, candidate, args.label);
1498
1542
  } catch (error) {
1499
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1500
- throw createTruncatedStructuredOutputError(args.label);
1501
- }
1502
- if (typeof candidate === "string" && candidate.trim().length > 0) {
1503
- throw createInvalidStructuredOutputError(args.label);
1504
- }
1505
- throw error;
1543
+ if (!(error instanceof import_errors32.ValidationError)) throw error;
1544
+ const failure = args.finishReason?.unified === "length" ? createTruncatedStructuredOutputError(args.label, error) : typeof candidate === "string" && !validJson ? createInvalidStructuredOutputError(args.label, error) : error;
1545
+ if (failure && typeof failure === "object")
1546
+ Object.assign(failure, {
1547
+ ...args.usage ? { usage: args.usage } : {},
1548
+ ...args.finishReason ? { finishReason: args.finishReason } : {}
1549
+ });
1550
+ throw failure;
1506
1551
  }
1507
1552
  };
1508
1553
  var parseInterruptFirstPassResult = (args) => {
1509
- const candidate = resolveOutputCandidate(args.text);
1510
- if (!isRecord22(candidate)) {
1511
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1512
- throw createTruncatedStructuredOutputError("useReason output");
1513
- }
1514
- if (typeof candidate === "string" && candidate.trim().length > 0) {
1515
- throw createInvalidStructuredOutputError("useReason output");
1516
- }
1517
- throw new Error(
1518
- "useReason first pass with interrupt must return a JSON object."
1519
- );
1520
- }
1521
- const mode = args.mode ?? "required";
1522
- let interruptRequired = true;
1523
- if (mode === "optional") {
1524
- if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
1525
- throw new Error(
1526
- 'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
1554
+ try {
1555
+ const candidate = resolveOutputCandidate(args.text);
1556
+ if (!isRecord22(candidate)) {
1557
+ if (args.finishReason?.unified === "length") {
1558
+ throw createTruncatedStructuredOutputError("useReason output");
1559
+ }
1560
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
1561
+ throw createInvalidStructuredOutputError("useReason output");
1562
+ }
1563
+ throw new import_errors32.ValidationError(
1564
+ "MODEL_OUTPUT_SCHEMA",
1565
+ "useReason first pass with interrupt must return a JSON object."
1527
1566
  );
1528
1567
  }
1529
- interruptRequired = candidate.decision === "interrupt";
1530
- if (!interruptRequired) {
1531
- const hasUnexpectedRequest = [
1568
+ const mode = args.mode ?? "required";
1569
+ let interruptRequired = true;
1570
+ if (mode === "optional") {
1571
+ if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
1572
+ throw new import_errors32.ValidationError(
1573
+ "MODEL_OUTPUT_SCHEMA",
1574
+ 'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
1575
+ );
1576
+ }
1577
+ interruptRequired = candidate.decision === "interrupt";
1578
+ if (!interruptRequired) {
1579
+ const hasUnexpectedRequest = [
1580
+ candidate.interruptRequest,
1581
+ candidate.request,
1582
+ candidate.interrupt
1583
+ ].some((value) => value !== null && value !== void 0);
1584
+ if (hasUnexpectedRequest) {
1585
+ throw new import_errors32.ValidationError(
1586
+ "MODEL_OUTPUT_SCHEMA",
1587
+ 'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
1588
+ );
1589
+ }
1590
+ }
1591
+ }
1592
+ let request;
1593
+ if (interruptRequired) {
1594
+ const requestCandidates = [
1532
1595
  candidate.interruptRequest,
1533
1596
  candidate.request,
1534
- candidate.interrupt
1535
- ].some((value) => value !== null && value !== void 0);
1536
- if (hasUnexpectedRequest) {
1537
- throw new Error(
1538
- 'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
1597
+ candidate.interrupt,
1598
+ mode === "required" ? candidate : void 0
1599
+ ];
1600
+ const requestPayload = requestCandidates.find(
1601
+ (value) => value !== null && value !== void 0
1602
+ );
1603
+ request = parseWithSchema(
1604
+ args.requestSchema,
1605
+ requestPayload,
1606
+ "useReason interrupt.request"
1607
+ );
1608
+ }
1609
+ let output;
1610
+ if (args.outputSchema) {
1611
+ const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
1612
+ try {
1613
+ output = parseWithSchema(
1614
+ args.outputSchema,
1615
+ outputPayload,
1616
+ "useReason output"
1539
1617
  );
1618
+ } catch (error) {
1619
+ if (!(error instanceof import_errors32.ValidationError)) throw error;
1620
+ if (args.finishReason?.unified === "length") {
1621
+ throw createTruncatedStructuredOutputError("useReason output", error);
1622
+ }
1623
+ throw error;
1540
1624
  }
1541
1625
  }
1542
- }
1543
- let request;
1544
- if (interruptRequired) {
1545
- const requestCandidates = [
1546
- candidate.interruptRequest,
1547
- candidate.request,
1548
- candidate.interrupt,
1549
- mode === "required" ? candidate : void 0
1550
- ];
1551
- const requestPayload = requestCandidates.find(
1552
- (value) => value !== null && value !== void 0
1553
- );
1554
- request = parseWithSchema(
1555
- args.requestSchema,
1556
- requestPayload,
1557
- "useReason interrupt.request"
1558
- );
1559
- }
1560
- let output;
1561
- if (args.outputSchema) {
1562
- const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
1563
- try {
1564
- output = parseWithSchema(
1565
- args.outputSchema,
1566
- outputPayload,
1567
- "useReason output"
1626
+ const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
1627
+ if (!args.outputSchema && !explicitDraft) {
1628
+ throw new import_errors32.ValidationError(
1629
+ "MODEL_OUTPUT_SCHEMA",
1630
+ "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
1568
1631
  );
1569
- } catch (error) {
1570
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1571
- throw createTruncatedStructuredOutputError("useReason output");
1572
- }
1573
- if (typeof outputPayload === "string" && outputPayload.trim().length > 0) {
1574
- throw createInvalidStructuredOutputError("useReason output");
1575
- }
1576
- throw error;
1577
1632
  }
1578
- }
1579
- const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
1580
- if (!args.outputSchema && !explicitDraft) {
1581
- throw new Error(
1582
- "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
1583
- );
1584
- }
1585
- const draftText = explicitDraft ?? (output !== void 0 ? tryStringify(output) : String(args.text ?? ""));
1586
- if (!interruptRequired) {
1633
+ const draftText = explicitDraft ?? (output !== void 0 ? tryStringify(output) : String(args.text ?? ""));
1634
+ if (!interruptRequired) {
1635
+ return {
1636
+ draftText,
1637
+ interruptRequired: false,
1638
+ ...output !== void 0 ? { output } : {}
1639
+ };
1640
+ }
1587
1641
  return {
1588
1642
  draftText,
1589
- interruptRequired: false,
1643
+ interruptRequired: true,
1644
+ request,
1590
1645
  ...output !== void 0 ? { output } : {}
1591
1646
  };
1647
+ } catch (error) {
1648
+ if (error && typeof error === "object")
1649
+ Object.assign(error, {
1650
+ ...args.usage ? { usage: args.usage } : {},
1651
+ ...args.finishReason ? { finishReason: args.finishReason } : {}
1652
+ });
1653
+ throw error;
1592
1654
  }
1593
- return {
1594
- draftText,
1595
- interruptRequired: true,
1596
- request,
1597
- ...output !== void 0 ? { output } : {}
1598
- };
1599
1655
  };
1600
1656
  var import_zod32 = require("zod");
1601
1657
  var tryStringify2 = (value) => {
@@ -1803,7 +1859,7 @@ ${tryStringify2(args.response)}`
1803
1859
  });
1804
1860
  };
1805
1861
  var import_core42 = require("@kortyx/core");
1806
- var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
1862
+ var import_errors42 = require("@kortyx/core/errors");
1807
1863
  var toToolDefinitions = (tools) => tools.map((tool) => ({
1808
1864
  name: tool.name,
1809
1865
  ...tool.title ? { title: tool.title } : {},
@@ -1837,17 +1893,27 @@ ${tryStringify2(args.response)}`
1837
1893
  ...value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value) ? { structuredContent: value } : {}
1838
1894
  };
1839
1895
  };
1840
- var closeOwnedTools = async (tools) => {
1896
+ var closeOwnedTools = async (tools, preserveFailure = false, traceSpan) => {
1841
1897
  const closeFns = /* @__PURE__ */ new Set();
1842
1898
  for (const tool of tools) {
1843
1899
  if (tool.closeAfterUse === false || !tool.close) continue;
1844
1900
  closeFns.add(tool.close);
1845
1901
  }
1846
- await Promise.all(
1847
- [...closeFns].map(async (close) => {
1848
- await close();
1849
- })
1850
- );
1902
+ try {
1903
+ await Promise.all(
1904
+ [...closeFns].map(async (close) => {
1905
+ await close();
1906
+ })
1907
+ );
1908
+ } catch (error) {
1909
+ if (!preserveFailure) throw error;
1910
+ try {
1911
+ traceSpan?.addEvent?.("useReason.cleanup.failed", {
1912
+ failure: (0, import_errors42.serializeFailure)(error)
1913
+ });
1914
+ } catch {
1915
+ }
1916
+ }
1851
1917
  };
1852
1918
  var createInitialMessages = (args) => [
1853
1919
  ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
@@ -1864,6 +1930,7 @@ ${tryStringify2(args.response)}`
1864
1930
  const saved = ctx.currentNodeState.byKey[checkpointKey];
1865
1931
  const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
1866
1932
  const opId = checkpoint?.opId ?? args.opId;
1933
+ let primaryFailure = false;
1867
1934
  const tools = useReasonArgs.tools ?? [];
1868
1935
  const toolByName = /* @__PURE__ */ new Map();
1869
1936
  let validationCompleted = false;
@@ -1896,7 +1963,7 @@ ${tryStringify2(args.response)}`
1896
1963
  validationCompleted = true;
1897
1964
  } finally {
1898
1965
  if (!validationCompleted) {
1899
- await closeOwnedTools(tools);
1966
+ await closeOwnedTools(tools, true, traceSpan);
1900
1967
  }
1901
1968
  }
1902
1969
  const toolDefinitions = toToolDefinitions(tools);
@@ -2015,6 +2082,7 @@ ${tryStringify2(args.response)}`
2015
2082
  text: step.text,
2016
2083
  schema: useReasonArgs.outputSchema,
2017
2084
  ...step.finishReason ? { finishReason: step.finishReason } : {},
2085
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2018
2086
  label: "useReason output"
2019
2087
  });
2020
2088
  aggregatedWarnings = mergeWarnings(aggregatedWarnings, [
@@ -2102,12 +2170,12 @@ ${tryStringify2(args.response)}`
2102
2170
  options: [
2103
2171
  { id: "approve", label: "Approve" },
2104
2172
  { id: "deny", label: "Deny" }
2105
- ],
2106
- meta: {
2107
- tool: tool.name,
2108
- toolCallId: toolCall.id,
2109
- input: toolCall.input
2110
- }
2173
+ ]
2174
+ },
2175
+ meta: {
2176
+ tool: tool.name,
2177
+ toolCallId: toolCall.id,
2178
+ input: toolCall.input
2111
2179
  }
2112
2180
  });
2113
2181
  const approved = Array.isArray(approval) ? approval.includes("approve") : approval === "approve";
@@ -2169,12 +2237,12 @@ ${tryStringify2(args.response)}`
2169
2237
  });
2170
2238
  } catch (error) {
2171
2239
  (0, import_core42.throwIfExecutionAborted)(abortSignal);
2172
- if ((0, import_core42.isExecutionLimitReached)(error) || (0, import_core42.isExecutionCancelled)(error))
2173
- throw error;
2240
+ if ((0, import_errors42.isControlFlowError)(error)) throw error;
2174
2241
  const result2 = {
2175
2242
  toolCallId: toolCall.id,
2176
2243
  name: tool.name,
2177
- content: toErrorMessage2(error),
2244
+ content: (0, import_errors42.serializeFailure)(error).message,
2245
+ failure: (0, import_errors42.serializeFailure)(error),
2178
2246
  isError: true
2179
2247
  };
2180
2248
  toolResults.push(result2);
@@ -2183,14 +2251,16 @@ ${tryStringify2(args.response)}`
2183
2251
  emitToolEvent("tool-call-error", {
2184
2252
  tool: tool.name,
2185
2253
  toolCallId: toolCall.id,
2186
- message: result2.content
2254
+ message: result2.content,
2255
+ failure: result2.failure
2187
2256
  });
2188
2257
  }
2189
2258
  traceSpan?.addEvent?.("useReason.tool-call.error", {
2190
2259
  stepIndex,
2191
2260
  tool: tool.name,
2192
2261
  toolCallId: toolCall.id,
2193
- message: result2.content
2262
+ message: result2.content,
2263
+ failure: result2.failure
2194
2264
  });
2195
2265
  }
2196
2266
  const completed2 = toolResults.at(-1);
@@ -2217,6 +2287,7 @@ ${tryStringify2(args.response)}`
2217
2287
  text: finalText,
2218
2288
  schema: useReasonArgs.outputSchema,
2219
2289
  ...finalFinishReason ? { finishReason: finalFinishReason } : {},
2290
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2220
2291
  label: "useReason output"
2221
2292
  });
2222
2293
  }
@@ -2263,15 +2334,19 @@ ${tryStringify2(args.response)}`
2263
2334
  ctx.stateDirty = true;
2264
2335
  return result;
2265
2336
  } catch (error) {
2266
- traceSpan?.fail?.(error, {
2267
- attributes: {
2268
- toolStepCount: steps.length,
2269
- toolCallCount: allToolCalls.length
2270
- }
2271
- });
2337
+ primaryFailure = true;
2338
+ try {
2339
+ traceSpan?.fail?.(error, {
2340
+ attributes: {
2341
+ toolStepCount: steps.length,
2342
+ toolCallCount: allToolCalls.length
2343
+ }
2344
+ });
2345
+ } catch {
2346
+ }
2272
2347
  throw error;
2273
2348
  } finally {
2274
- await closeOwnedTools(tools);
2349
+ await closeOwnedTools(tools, primaryFailure, traceSpan);
2275
2350
  }
2276
2351
  };
2277
2352
  var shouldStreamReasonStructured = (args) => (args.emit ?? true) && shouldStreamStructured(args.structured);
@@ -2434,6 +2509,7 @@ ${tryStringify2(args.response)}`
2434
2509
  text: first.text,
2435
2510
  requestSchema: args.interrupt.requestSchema,
2436
2511
  ...first.finishReason ? { finishReason: first.finishReason } : {},
2512
+ ...first.usage ? { usage: first.usage } : {},
2437
2513
  ...args.outputSchema ? { outputSchema: args.outputSchema } : {},
2438
2514
  mode: args.interrupt.mode ?? "required"
2439
2515
  });
@@ -2451,6 +2527,7 @@ ${tryStringify2(args.response)}`
2451
2527
  text: first.text,
2452
2528
  schema: args.outputSchema,
2453
2529
  ...first.finishReason ? { finishReason: first.finishReason } : {},
2530
+ ...first.usage ? { usage: first.usage } : {},
2454
2531
  label: "useReason output"
2455
2532
  });
2456
2533
  }
@@ -2545,6 +2622,7 @@ ${tryStringify2(args.response)}`
2545
2622
  text: finalText,
2546
2623
  schema: args.outputSchema,
2547
2624
  ...second.finishReason ? { finishReason: second.finishReason } : {},
2625
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2548
2626
  label: "useReason output"
2549
2627
  });
2550
2628
  }
@@ -2647,8 +2725,176 @@ ${tryStringify2(args.response)}`
2647
2725
  };
2648
2726
  return [getValue(), setValue];
2649
2727
  }
2650
- var import_node_crypto5 = require("crypto");
2651
2728
  var import_core62 = require("@kortyx/core");
2729
+ var import_errors52 = require("@kortyx/core/errors");
2730
+ var ParallelChildWaiting = class extends Error {
2731
+ name = "ParallelChildWaiting";
2732
+ };
2733
+ var ParallelError = class extends AggregateError {
2734
+ constructor(results) {
2735
+ super(
2736
+ results.flatMap(
2737
+ (result) => result.status === "rejected" ? [result.reason] : []
2738
+ ),
2739
+ "One or more parallel operations failed."
2740
+ );
2741
+ this.results = results;
2742
+ this.name = "ParallelError";
2743
+ this.failure = (0, import_errors52.serializeFailure)({
2744
+ version: 1,
2745
+ code: this.code,
2746
+ category: "workflow",
2747
+ message: this.message,
2748
+ retryable: null,
2749
+ results: results.map(
2750
+ (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors52.serializeFailure)(result.reason) }
2751
+ )
2752
+ });
2753
+ }
2754
+ results;
2755
+ code = "PARALLEL_FAILED";
2756
+ failure;
2757
+ toJSON() {
2758
+ return (0, import_errors52.serializeFailure)(this);
2759
+ }
2760
+ };
2761
+ function registerWorkflowTask(id) {
2762
+ const ctx = getHookContext();
2763
+ if (ctx.workflowControlError) throw ctx.workflowControlError;
2764
+ if (ctx.workflowContextClosed)
2765
+ throw new Error("The workflow node activation has finished.");
2766
+ const group = ctx.parallelGroup;
2767
+ if (group?.restored && !group.record.calls.includes(id)) {
2768
+ throw new Error(`Parallel call membership changed during replay: '${id}'.`);
2769
+ }
2770
+ let finish;
2771
+ const task = {
2772
+ id,
2773
+ ...group ? { group } : {},
2774
+ completed: false,
2775
+ done: new Promise((resolve2) => {
2776
+ finish = resolve2;
2777
+ }),
2778
+ finish: () => {
2779
+ task.completed = true;
2780
+ finish();
2781
+ }
2782
+ };
2783
+ ctx.workflowTasks.push(task);
2784
+ if (group && !group.record.calls.includes(id)) group.record.calls.push(id);
2785
+ return task;
2786
+ }
2787
+ function parallel(values) {
2788
+ const settled = Promise.allSettled(values);
2789
+ let ctx;
2790
+ try {
2791
+ ctx = getHookContext();
2792
+ } catch (error) {
2793
+ return Promise.reject(error);
2794
+ }
2795
+ const join = async () => {
2796
+ if (ctx.workflowControlError) throw ctx.workflowControlError;
2797
+ if (ctx.parallelGroup)
2798
+ throw new Error(
2799
+ "Await each parallel group before starting another in the same node."
2800
+ );
2801
+ const key = `__parallel:${ctx.parallelIndex++}`;
2802
+ const saved = ctx.currentNodeState.byKey[key];
2803
+ const tasks = ctx.workflowTasks.filter(
2804
+ (task) => !task.completed && !task.group
2805
+ );
2806
+ const ids = tasks.map((task) => task.id);
2807
+ if (saved && (saved.version !== 1 || ids.some((id) => !saved.calls.includes(id)))) {
2808
+ throw new Error("Parallel call membership changed during replay.");
2809
+ }
2810
+ const group = {
2811
+ key,
2812
+ record: saved ?? { version: 1, calls: ids, interrupts: [] },
2813
+ restored: Boolean(saved),
2814
+ ready: Promise.resolve()
2815
+ };
2816
+ ctx.currentNodeState.byKey[key] = group.record;
2817
+ ctx.stateDirty = true;
2818
+ ctx.parallelGroup = group;
2819
+ for (const task of tasks) task.group = group;
2820
+ const callRecord = (id) => ctx.currentNodeState.byKey[`__useWorkflow:${id}`];
2821
+ const bridge = async (callId, index) => {
2822
+ const interruption = callRecord(callId)?.interrupts[index];
2823
+ if (!interruption)
2824
+ throw new Error(
2825
+ "Parallel interrupt history does not match the saved child."
2826
+ );
2827
+ try {
2828
+ interruption.response = await awaitInterruptInternal({
2829
+ request: interruption.request,
2830
+ ...interruption.request.meta ? { meta: interruption.request.meta } : {}
2831
+ });
2832
+ } catch (error) {
2833
+ ctx.workflowControlError = error;
2834
+ throw error;
2835
+ }
2836
+ };
2837
+ group.ready = (async () => {
2838
+ for (const entry of group.record.interrupts)
2839
+ await bridge(entry.callId, entry.index);
2840
+ })();
2841
+ try {
2842
+ const [ready, outcomes] = await Promise.all([
2843
+ Promise.allSettled([group.ready]),
2844
+ settled
2845
+ ]);
2846
+ let drained = -1;
2847
+ while (drained !== ctx.workflowTasks.length) {
2848
+ drained = ctx.workflowTasks.length;
2849
+ await Promise.all(
2850
+ ctx.workflowTasks.filter((task) => task.group === group).map((task) => task.done)
2851
+ );
2852
+ }
2853
+ if (ready[0]?.status === "rejected") throw ready[0].reason;
2854
+ (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
2855
+ if (ctx.workflowControlError) throw ctx.workflowControlError;
2856
+ const rejected = outcomes.filter(
2857
+ (outcome) => outcome.status === "rejected"
2858
+ );
2859
+ const control = rejected.find(
2860
+ (outcome) => (0, import_core62.isExecutionCancelled)(outcome.reason) || (0, import_core62.isExecutionLimitReached)(outcome.reason)
2861
+ );
2862
+ if (control) {
2863
+ ctx.workflowControlError = control.reason;
2864
+ throw control.reason;
2865
+ }
2866
+ const actual = ctx.workflowTasks.filter((task) => task.group === group).map((task) => task.id);
2867
+ if (group.record.calls.length !== actual.length || group.record.calls.some((id) => !actual.includes(id))) {
2868
+ throw new Error("Parallel call membership changed during replay.");
2869
+ }
2870
+ for (const callId of group.record.calls) {
2871
+ const record = callRecord(callId);
2872
+ if (record?.status !== "interrupted") continue;
2873
+ const index = record.interrupts.length - 1;
2874
+ if (record.interrupts[index]?.response !== void 0) continue;
2875
+ group.record.interrupts.push({ callId, index });
2876
+ await bridge(callId, index);
2877
+ throw new Error(
2878
+ "Parallel interrupt unexpectedly resumed without replay."
2879
+ );
2880
+ }
2881
+ if (rejected.length) throw new ParallelError(outcomes);
2882
+ return outcomes.map(
2883
+ (outcome) => outcome.value
2884
+ );
2885
+ } finally {
2886
+ ctx.parallelGroup = void 0;
2887
+ }
2888
+ };
2889
+ const promise = join();
2890
+ ctx.parallelJoins.push(promise);
2891
+ void promise.catch(() => {
2892
+ });
2893
+ return promise;
2894
+ }
2895
+ var import_node_crypto5 = require("crypto");
2896
+ var import_core7 = require("@kortyx/core");
2897
+ var import_errors62 = require("@kortyx/core/errors");
2652
2898
  var import_node_crypto42 = require("crypto");
2653
2899
  function emitWorkflowCall2(telemetry, type, payload) {
2654
2900
  const base = telemetry?.correlation;
@@ -2686,10 +2932,20 @@ ${tryStringify2(args.response)}`
2686
2932
  const serialized = JSON.stringify(value);
2687
2933
  return serialized && Buffer.byteLength(serialized, "utf8") <= 16384 ? { [side]: JSON.parse(serialized) } : { [`${side}Omitted`]: "Content exceeds 16 KiB" };
2688
2934
  }
2689
- var WorkflowCallError = class extends Error {
2690
- constructor(workflow, message) {
2691
- super(`Workflow '${workflow}': ${message}`);
2935
+ var WorkflowCallError = class extends import_errors62.KortyxError {
2936
+ constructor(workflow, message, cause, failure) {
2937
+ const detail = (0, import_errors62.serializeFailure)(
2938
+ failure ?? cause ?? new import_errors62.WorkflowContractError(message)
2939
+ );
2940
+ super(detail.code, `Workflow '${workflow}': ${message}`, {
2941
+ ...detail,
2942
+ safeMessage: detail.message,
2943
+ ...cause !== void 0 ? { cause } : {}
2944
+ });
2692
2945
  this.workflow = workflow;
2946
+ Object.assign(this.failure, detail, {
2947
+ context: { ...detail.context, workflow }
2948
+ });
2693
2949
  this.name = "WorkflowCallError";
2694
2950
  }
2695
2951
  workflow;
@@ -2700,10 +2956,12 @@ ${tryStringify2(args.response)}`
2700
2956
  if (v === null || typeof v === "string" || typeof v === "boolean") return v;
2701
2957
  if (typeof v === "number" && Number.isFinite(v)) return v;
2702
2958
  if (typeof v !== "object" || !v || seen.has(v)) {
2703
- throw new Error("Workflow input and output must be JSON serializable.");
2959
+ throw new import_errors62.WorkflowContractError(
2960
+ "Workflow input and output must be JSON serializable."
2961
+ );
2704
2962
  }
2705
2963
  if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype) {
2706
- throw new Error(
2964
+ throw new import_errors62.WorkflowContractError(
2707
2965
  "Workflow input and output must contain plain JSON objects."
2708
2966
  );
2709
2967
  }
@@ -2724,29 +2982,49 @@ ${tryStringify2(args.response)}`
2724
2982
  useWorkflow: (args) => {
2725
2983
  const workflow = workflows[args.workflow];
2726
2984
  if (!workflow || workflow.id !== args.workflow)
2727
- throw new Error("Workflow registry keys must match definition ids.");
2985
+ throw new import_errors62.WorkflowContractError(
2986
+ "Workflow registry keys must match definition ids."
2987
+ );
2728
2988
  return useWorkflow({ ...args, workflow });
2729
2989
  }
2730
2990
  };
2731
2991
  }
2732
2992
  async function useWorkflow(args) {
2993
+ const task = registerWorkflowTask(args.id);
2994
+ try {
2995
+ await Promise.resolve();
2996
+ if (task.group) await task.group.ready;
2997
+ return await runWorkflowCall(args, task);
2998
+ } catch (error) {
2999
+ if ((0, import_core7.isExecutionCancelled)(error) || (0, import_core7.isExecutionLimitReached)(error))
3000
+ getHookContext().workflowControlError = error;
3001
+ throw error;
3002
+ } finally {
3003
+ task.finish();
3004
+ }
3005
+ }
3006
+ async function runWorkflowCall(args, task) {
2733
3007
  const ctx = getHookContext();
2734
- (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
3008
+ (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
2735
3009
  const service = ctx.node.callWorkflow;
2736
3010
  if (!service)
2737
- throw new Error("useWorkflow requires an agent workflow registry.");
3011
+ throw new import_errors62.WorkflowContractError(
3012
+ "useWorkflow requires an agent workflow registry."
3013
+ );
2738
3014
  if (!args.id.trim())
2739
- throw new Error("useWorkflow requires a stable, nonempty id.");
2740
- if (ctx.workflowCallActive)
2741
- throw new Error(
2742
- "Concurrent workflow calls are not supported; await each call."
3015
+ throw new import_errors62.WorkflowContractError(
3016
+ "useWorkflow requires a stable, nonempty id."
3017
+ );
3018
+ if (ctx.workflowCallActive && !task.group)
3019
+ throw new import_errors62.WorkflowContractError(
3020
+ "Concurrent workflow calls require parallel([...]); otherwise await each call."
2743
3021
  );
2744
3022
  if (ctx.workflowCallIds.has(args.id))
2745
- throw new Error(
3023
+ throw new import_errors62.WorkflowContractError(
2746
3024
  `Duplicate workflow call id '${args.id}' in one node activation.`
2747
3025
  );
2748
3026
  if (ctx.workflowCallIds.size >= 64)
2749
- throw new Error(
3027
+ throw new import_errors62.WorkflowContractError(
2750
3028
  "A node may call at most 64 child workflows per activation."
2751
3029
  );
2752
3030
  ctx.workflowCallIds.add(args.id);
@@ -2760,6 +3038,11 @@ ${tryStringify2(args.response)}`
2760
3038
  });
2761
3039
  const key = `__useWorkflow:${args.id}`;
2762
3040
  let record = ctx.currentNodeState.byKey[key];
3041
+ if (record && record.parallelKey !== task.group?.key)
3042
+ throw new WorkflowCallError(
3043
+ workflow,
3044
+ "parallel call membership changed during replay"
3045
+ );
2763
3046
  if (record && record.fingerprint !== fingerprint)
2764
3047
  throw new WorkflowCallError(
2765
3048
  workflow,
@@ -2770,11 +3053,12 @@ ${tryStringify2(args.response)}`
2770
3053
  fingerprint,
2771
3054
  invocationId: (0, import_node_crypto5.randomUUID)(),
2772
3055
  status: "running",
2773
- interrupts: []
3056
+ interrupts: [],
3057
+ ...task.group ? { parallelKey: task.group.key } : {}
2774
3058
  };
2775
3059
  ctx.currentNodeState.byKey[key] = record;
2776
3060
  ctx.stateDirty = true;
2777
- ctx.workflowCallActive = true;
3061
+ if (!task.group) ctx.workflowCallActive = true;
2778
3062
  const telemetry = ctx.node.workflowCallTelemetry;
2779
3063
  const activationKey = "__useWorkflowActivation";
2780
3064
  ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto5.randomUUID)();
@@ -2810,14 +3094,21 @@ ${tryStringify2(args.response)}`
2810
3094
  };
2811
3095
  const cached = record.status === "completed";
2812
3096
  try {
2813
- for (const interruption of record.interrupts) {
3097
+ for (const interruption of task.group ? [] : record.interrupts) {
2814
3098
  interruption.response = await awaitInterruptInternal({
2815
3099
  request: interruption.request,
2816
3100
  ...interruption.request.meta ? { meta: interruption.request.meta } : {}
2817
3101
  });
2818
3102
  }
2819
3103
  if (record.status === "failed")
2820
- throw new WorkflowCallError(workflow, record.error ?? "child failed");
3104
+ throw new WorkflowCallError(
3105
+ workflow,
3106
+ record.error ?? "child failed",
3107
+ void 0,
3108
+ record.failure ?? (0, import_errors62.serializeFailure)(void 0)
3109
+ );
3110
+ if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
3111
+ throw new ParallelChildWaiting();
2821
3112
  while (record.status !== "completed") {
2822
3113
  let outcome;
2823
3114
  try {
@@ -2836,13 +3127,13 @@ ${tryStringify2(args.response)}`
2836
3127
  ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
2837
3128
  });
2838
3129
  } catch (error) {
2839
- (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
2840
- if ((0, import_core62.isExecutionCancelled)(error)) throw error;
2841
- if ((0, import_core62.isExecutionLimitReached)(error)) {
2842
- const child = error;
2843
- if (child.__kortyxChildSnapshot)
3130
+ const child = error;
3131
+ recordUsage(child?.__kortyxChildUsage);
3132
+ (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
3133
+ if ((0, import_core7.isExecutionCancelled)(error)) throw error;
3134
+ if ((0, import_core7.isExecutionLimitReached)(error)) {
3135
+ if (child?.__kortyxChildSnapshot)
2844
3136
  record.snapshot = child.__kortyxChildSnapshot;
2845
- recordUsage(child.__kortyxChildUsage);
2846
3137
  report("suspended", {
2847
3138
  reason: "limit_reached",
2848
3139
  limit: error.limitReached
@@ -2850,11 +3141,17 @@ ${tryStringify2(args.response)}`
2850
3141
  throw error;
2851
3142
  }
2852
3143
  record.status = "failed";
2853
- record.error = error instanceof Error ? error.message : String(error);
3144
+ record.failure = (0, import_errors62.serializeFailure)(error);
3145
+ record.error = record.failure.message;
2854
3146
  report("failed", { error: "Child workflow failed" });
2855
- throw new WorkflowCallError(workflow, record.error);
3147
+ throw new WorkflowCallError(
3148
+ workflow,
3149
+ record.error,
3150
+ error,
3151
+ record.failure
3152
+ );
2856
3153
  }
2857
- (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
3154
+ (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
2858
3155
  recordUsage(outcome.usage);
2859
3156
  if (outcome.status === "completed") {
2860
3157
  record.data = outcome.data;
@@ -2875,6 +3172,7 @@ ${tryStringify2(args.response)}`
2875
3172
  request: outcome.request
2876
3173
  };
2877
3174
  record.interrupts.push(interruption);
3175
+ if (task.group) throw new ParallelChildWaiting();
2878
3176
  interruption.response = await awaitInterruptInternal({
2879
3177
  request: interruption.request,
2880
3178
  ...interruption.request.meta ? { meta: interruption.request.meta } : {}
@@ -2888,7 +3186,7 @@ ${tryStringify2(args.response)}`
2888
3186
  data: JSON.parse(JSON.stringify(data))
2889
3187
  };
2890
3188
  } finally {
2891
- ctx.workflowCallActive = false;
3189
+ if (!task.group) ctx.workflowCallActive = false;
2892
3190
  }
2893
3191
  }
2894
3192
  }
@@ -2901,6 +3199,7 @@ __export(index_exports, {
2901
3199
  createAgent: () => createAgent,
2902
3200
  createChatRouteHandler: () => createChatRouteHandler,
2903
3201
  createCheckpointRouteHandler: () => createCheckpointRouteHandler,
3202
+ createFailureResponse: () => createFailureResponse,
2904
3203
  extractLatestUserMessage: () => extractLatestUserMessage,
2905
3204
  handleChatRequestBody: () => handleChatRequestBody,
2906
3205
  handleCheckpointRequestBody: () => handleCheckpointRequestBody,
@@ -2909,6 +3208,7 @@ __export(index_exports, {
2909
3208
  parseCheckpointRequestBody: () => parseCheckpointRequestBody,
2910
3209
  parseResumeMeta: () => parseResumeMeta,
2911
3210
  projectWorkflowTopology: () => projectWorkflowTopology,
3211
+ readRequestJson: () => readRequestJson,
2912
3212
  streamChat: () => streamChat,
2913
3213
  streamChatFromRoute: () => streamChatFromRoute,
2914
3214
  transformGraphStreamForUI: () => transformGraphStreamForUI,
@@ -2917,6 +3217,7 @@ __export(index_exports, {
2917
3217
  module.exports = __toCommonJS(index_exports);
2918
3218
 
2919
3219
  // src/adapters/http.ts
3220
+ var import_errors = require("@kortyx/core/errors");
2920
3221
  var import_stream = require("@kortyx/stream");
2921
3222
  var import_zod = require("zod");
2922
3223
  var chatMessageSchema = import_zod.z.object({
@@ -2952,12 +3253,41 @@ var checkpointRequestBodySchema = import_zod.z.discriminatedUnion("action", [
2952
3253
  newSessionId: import_zod.z.string().min(1).optional()
2953
3254
  }).strict()
2954
3255
  ]);
2955
- var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
3256
+ async function readRequestJson(request) {
3257
+ try {
3258
+ const value = await request.json();
3259
+ if (!value || typeof value !== "object" || Array.isArray(value))
3260
+ throw new Error("Expected a JSON object.");
3261
+ return value;
3262
+ } catch (cause) {
3263
+ throw new import_errors.KortyxError(
3264
+ "INVALID_REQUEST",
3265
+ "Request body is not valid JSON.",
3266
+ {
3267
+ category: "request",
3268
+ retryable: false,
3269
+ safeMessage: "The request body must contain a valid JSON object.",
3270
+ cause
3271
+ }
3272
+ );
3273
+ }
3274
+ }
3275
+ function createFailureResponse(error, status) {
3276
+ const failure = (0, import_errors.serializeFailure)(error);
3277
+ return Response.json(
3278
+ { error: failure.message, failure },
3279
+ { status: status ?? (0, import_errors.failureHttpStatus)(failure) }
3280
+ );
3281
+ }
2956
3282
  function parseChatRequestBody(value) {
2957
3283
  const parsed = chatRequestBodySchema.safeParse(value);
2958
3284
  if (!parsed.success) {
2959
3285
  const [firstIssue] = parsed.error.issues;
2960
- throw new Error(firstIssue.message);
3286
+ throw new import_errors.KortyxError("INVALID_REQUEST", firstIssue.message, {
3287
+ category: "request",
3288
+ retryable: false,
3289
+ safeMessage: "The request body is invalid."
3290
+ });
2961
3291
  }
2962
3292
  const sessionId = parsed.data.sessionId?.trim();
2963
3293
  const workflowId = parsed.data.workflowId?.trim();
@@ -2973,7 +3303,11 @@ function parseCheckpointRequestBody(value) {
2973
3303
  const parsed = checkpointRequestBodySchema.safeParse(value);
2974
3304
  if (parsed.success) return parsed.data;
2975
3305
  const [firstIssue] = parsed.error.issues;
2976
- throw new Error(firstIssue.message);
3306
+ throw new import_errors.KortyxError("INVALID_REQUEST", firstIssue.message, {
3307
+ category: "request",
3308
+ retryable: false,
3309
+ safeMessage: "The request body is invalid."
3310
+ });
2977
3311
  }
2978
3312
  async function handleChatRequestBody(args) {
2979
3313
  const { agent, body } = args;
@@ -2995,10 +3329,10 @@ async function handleChatRequestBody(args) {
2995
3329
  return (0, import_stream.toSSE)(stream);
2996
3330
  }
2997
3331
  function createChatRouteHandler(args) {
2998
- const { agent, errorStatus = 400 } = args;
3332
+ const { agent, errorStatus } = args;
2999
3333
  return async function POST(request) {
3000
3334
  try {
3001
- const body = parseChatRequestBody(await request.json());
3335
+ const body = parseChatRequestBody(await readRequestJson(request));
3002
3336
  return await handleChatRequestBody({
3003
3337
  agent,
3004
3338
  body,
@@ -3006,17 +3340,7 @@ function createChatRouteHandler(args) {
3006
3340
  onExecution: args.onExecution
3007
3341
  });
3008
3342
  } catch (error) {
3009
- return new Response(
3010
- JSON.stringify({
3011
- error: toErrorMessage(error)
3012
- }),
3013
- {
3014
- status: errorStatus,
3015
- headers: {
3016
- "content-type": "application/json"
3017
- }
3018
- }
3019
- );
3343
+ return createFailureResponse(error, errorStatus);
3020
3344
  }
3021
3345
  };
3022
3346
  }
@@ -3032,23 +3356,13 @@ async function handleCheckpointRequestBody(args) {
3032
3356
  });
3033
3357
  }
3034
3358
  function createCheckpointRouteHandler(args) {
3035
- const { agent, errorStatus = 400 } = args;
3359
+ const { agent, errorStatus } = args;
3036
3360
  return async function POST(request) {
3037
3361
  try {
3038
- const body = parseCheckpointRequestBody(await request.json());
3362
+ const body = parseCheckpointRequestBody(await readRequestJson(request));
3039
3363
  return await handleCheckpointRequestBody({ agent, body });
3040
3364
  } catch (error) {
3041
- return new Response(
3042
- JSON.stringify({
3043
- error: toErrorMessage(error)
3044
- }),
3045
- {
3046
- status: errorStatus,
3047
- headers: {
3048
- "content-type": "application/json"
3049
- }
3050
- }
3051
- );
3365
+ return createFailureResponse(error, errorStatus);
3052
3366
  }
3053
3367
  };
3054
3368
  }
@@ -3077,6 +3391,7 @@ var import_zod5 = require("zod");
3077
3391
 
3078
3392
  // src/execution/execute.ts
3079
3393
  var import_core4 = require("@kortyx/core");
3394
+ var import_errors5 = require("@kortyx/core/errors");
3080
3395
  var import_runtime3 = require("@kortyx/runtime");
3081
3396
  var import_zod3 = require("zod");
3082
3397
 
@@ -3084,32 +3399,37 @@ var import_zod3 = require("zod");
3084
3399
  var import_core3 = require("@kortyx/core");
3085
3400
  var import_runtime2 = require("@kortyx/runtime");
3086
3401
 
3402
+ // src/execution/types.ts
3403
+ var import_errors2 = require("@kortyx/core/errors");
3404
+ var ExecutionRequestError = class extends import_errors2.KortyxError {
3405
+ name = "ExecutionRequestError";
3406
+ constructor(code, message, cause) {
3407
+ super(code, message, {
3408
+ category: "request",
3409
+ retryable: false,
3410
+ safeMessage: "The execution request is invalid. Check the workflow contract and request fields.",
3411
+ cause
3412
+ });
3413
+ }
3414
+ };
3415
+
3087
3416
  // src/orchestrator.ts
3088
3417
  var import_node_stream = require("stream");
3089
3418
  var import_core2 = require("@kortyx/core");
3419
+ var import_errors4 = require("@kortyx/core/errors");
3090
3420
  var import_runtime = require("@kortyx/runtime");
3091
3421
  var import_langgraph = require("@langchain/langgraph");
3092
3422
 
3093
3423
  // src/execution/contracts.ts
3424
+ var import_errors3 = require("@kortyx/core/errors");
3094
3425
  var import_zod2 = require("zod");
3095
-
3096
- // src/execution/types.ts
3097
- var ExecutionRequestError = class extends Error {
3098
- constructor(code, message) {
3099
- super(message);
3100
- this.code = code;
3101
- }
3102
- code;
3103
- name = "ExecutionRequestError";
3104
- };
3105
-
3106
- // src/execution/contracts.ts
3107
3426
  async function resolveExecutionWorkflow(select, reference) {
3108
3427
  const id = typeof reference === "string" ? reference : reference.id;
3109
3428
  let workflow;
3110
3429
  try {
3111
3430
  workflow = await select(id);
3112
- } catch {
3431
+ } catch (error) {
3432
+ if ((0, import_errors3.errorProperty)(error, "code") !== "UNKNOWN_WORKFLOW") throw error;
3113
3433
  throw new ExecutionRequestError(
3114
3434
  "UNKNOWN_WORKFLOW",
3115
3435
  `Workflow '${id}' is not registered.`
@@ -3139,10 +3459,8 @@ function parseExecutionInput(workflow, input) {
3139
3459
  import_zod2.z.json().parse(parsed);
3140
3460
  return parsed;
3141
3461
  } catch (error) {
3142
- throw new ExecutionRequestError(
3143
- "INVALID_INPUT",
3144
- error instanceof Error ? error.message : String(error)
3145
- );
3462
+ if (!(error instanceof import_zod2.z.ZodError)) throw error;
3463
+ throw new ExecutionRequestError("INVALID_INPUT", error.message, error);
3146
3464
  }
3147
3465
  }
3148
3466
  async function validateExecutionOutput(state, select) {
@@ -3151,7 +3469,9 @@ async function validateExecutionOutput(state, select) {
3151
3469
  const contract = state.config.executionContract;
3152
3470
  const root = await select(contract.id);
3153
3471
  if (root.id !== contract.id || root.version !== contract.version)
3154
- throw new Error("The root workflow changed since execution started.");
3472
+ throw new import_errors3.WorkflowContractError(
3473
+ "The root workflow changed since execution started."
3474
+ );
3155
3475
  let data = state.data ?? {};
3156
3476
  try {
3157
3477
  for (const schema of /* @__PURE__ */ new Set([terminal.outputSchema, root.outputSchema])) {
@@ -3159,10 +3479,12 @@ async function validateExecutionOutput(state, select) {
3159
3479
  }
3160
3480
  if (terminal.outputSchema || root.outputSchema) import_zod2.z.json().parse(data);
3161
3481
  } catch (error) {
3162
- const failure = new Error(
3163
- `Workflow output validation failed: ${error instanceof Error ? error.message : String(error)}`
3482
+ if (!(error instanceof import_zod2.z.ZodError)) throw error;
3483
+ const failure = new import_errors3.ValidationError(
3484
+ "INVALID_OUTPUT",
3485
+ `Workflow output validation failed: ${error.message}`,
3486
+ error
3164
3487
  );
3165
- Object.assign(failure, { code: "INVALID_OUTPUT" });
3166
3488
  throw failure;
3167
3489
  }
3168
3490
  return { ...state, data };
@@ -3674,7 +3996,8 @@ async function orchestrateGraphStream({
3674
3996
  interruptId,
3675
3997
  resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
3676
3998
  resumeOutcome: "failed",
3677
- resumeError: resumeError instanceof Error ? resumeError.message : String(resumeError)
3999
+ resumeError: (0, import_errors4.serializeFailure)(resumeError).message,
4000
+ failure: (0, import_errors4.serializeFailure)(resumeError)
3678
4001
  },
3679
4002
  flush: true
3680
4003
  });
@@ -3745,8 +4068,10 @@ async function orchestrateGraphStream({
3745
4068
  if (response.closed) outcomeState.config.responseCompleted = true;
3746
4069
  if (typeof frameworkAdapter?.checkpointer?.getTuple !== "function") return;
3747
4070
  for (const [token, request] of activePendingRequests) {
3748
- if (budget) request.state.config.executionBudget = budget;
3749
- if (response.closed) request.state.config.responseCompleted = true;
4071
+ if (budget && request.state)
4072
+ request.state.config.executionBudget = budget;
4073
+ if (response.closed && request.state)
4074
+ request.state.config.responseCompleted = true;
3750
4075
  const graphSnapshot = await (0, import_runtime.captureGraphSnapshot)(
3751
4076
  frameworkAdapter.checkpointer,
3752
4077
  runId,
@@ -3813,7 +4138,10 @@ async function orchestrateGraphStream({
3813
4138
  if (pendingStore) {
3814
4139
  const savePromise = pendingStore.save(record).catch((error) => {
3815
4140
  outcomeError = error;
3816
- console.error("[orchestrator] failed to save pending request", error);
4141
+ console.error(
4142
+ "[orchestrator] failed to save pending request",
4143
+ (0, import_errors4.serializeFailure)(error)
4144
+ );
3817
4145
  });
3818
4146
  pendingRequestWrites.push(savePromise);
3819
4147
  }
@@ -3885,8 +4213,13 @@ async function orchestrateGraphStream({
3885
4213
  const msg = String(
3886
4214
  payload?.message ?? "Unexpected error"
3887
4215
  );
3888
- outcomeError = new Error(msg);
3889
- write({ type: "error", message: msg });
4216
+ const supplied = payload?.failure;
4217
+ outcomeError = (0, import_errors4.isFailureDescriptor)(supplied) ? (0, import_errors4.errorFromFailure)(supplied) : new Error(msg);
4218
+ write({
4219
+ type: "error",
4220
+ message: (0, import_errors4.serializeFailure)(outcomeError).message,
4221
+ failure: (0, import_errors4.serializeFailure)(outcomeError)
4222
+ });
3890
4223
  write({ type: "done" });
3891
4224
  finished = true;
3892
4225
  out.end();
@@ -4058,7 +4391,10 @@ async function orchestrateGraphStream({
4058
4391
  input: p?.input
4059
4392
  };
4060
4393
  void persistAndEmitInterrupt(local).catch((error) => {
4061
- console.error("[orchestrator] failed to emit interrupt", error);
4394
+ console.error(
4395
+ "[orchestrator] failed to emit interrupt",
4396
+ (0, import_errors4.serializeFailure)(error)
4397
+ );
4062
4398
  });
4063
4399
  return;
4064
4400
  }
@@ -4245,7 +4581,8 @@ async function orchestrateGraphStream({
4245
4581
  });
4246
4582
  write({
4247
4583
  type: "error",
4248
- message: `Transition failed to '${transitionTo}': ${err instanceof Error ? err.message : String(err)}`
4584
+ message: (0, import_errors4.serializeFailure)(err).message,
4585
+ failure: (0, import_errors4.serializeFailure)(err)
4249
4586
  });
4250
4587
  write({ type: "done" });
4251
4588
  out.end();
@@ -4294,7 +4631,10 @@ async function orchestrateGraphStream({
4294
4631
  }
4295
4632
  }
4296
4633
  } catch (e) {
4297
- console.error("[orchestrator] framework cleanup failed", e);
4634
+ console.error(
4635
+ "[orchestrator] framework cleanup failed",
4636
+ (0, import_errors4.serializeFailure)(e)
4637
+ );
4298
4638
  }
4299
4639
  }
4300
4640
  outcomeState = workflowFinalState;
@@ -4345,7 +4685,10 @@ async function orchestrateGraphStream({
4345
4685
  });
4346
4686
  } catch (error) {
4347
4687
  outcomeError = error;
4348
- console.error("[orchestrator] session checkpoint failed", error);
4688
+ console.error(
4689
+ "[orchestrator] session checkpoint failed",
4690
+ (0, import_errors4.serializeFailure)(error)
4691
+ );
4349
4692
  }
4350
4693
  }
4351
4694
  write({ type: "done", data: workflowFinalState });
@@ -4414,7 +4757,10 @@ async function orchestrateGraphStream({
4414
4757
  });
4415
4758
  } catch (error) {
4416
4759
  outcomeError = error;
4417
- console.error("[orchestrator] session checkpoint failed", error);
4760
+ console.error(
4761
+ "[orchestrator] session checkpoint failed",
4762
+ (0, import_errors4.serializeFailure)(error)
4763
+ );
4418
4764
  }
4419
4765
  }
4420
4766
  runTraceSpan.setAttributes?.({
@@ -4464,7 +4810,7 @@ async function orchestrateGraphStream({
4464
4810
  try {
4465
4811
  await frameworkAdapter?.cleanupRun?.(runId, [...namespacesUsed]);
4466
4812
  } catch (cleanupError) {
4467
- console.error("[cancel:cleanupRun]", cleanupError);
4813
+ console.error("[cancel:cleanupRun]", (0, import_errors4.serializeFailure)(cleanupError));
4468
4814
  }
4469
4815
  outcomeError = (0, import_core2.createExecutionCancelledError)();
4470
4816
  fallbackRunSpan?.setAttributes?.({ "kortyx.run.cancelled": true });
@@ -4594,10 +4940,11 @@ async function orchestrateGraphStream({
4594
4940
  outcomeError = err;
4595
4941
  fallbackRunSpan?.fail?.(err);
4596
4942
  emitResumeFailure(err);
4597
- console.error("[error:orchestrateGraphStream]", err);
4943
+ console.error("[error:orchestrateGraphStream]", (0, import_errors4.serializeFailure)(err));
4598
4944
  write({
4599
4945
  type: "error",
4600
- message: err instanceof Error ? err.message : String(err)
4946
+ message: (0, import_errors4.serializeFailure)(err).message,
4947
+ failure: (0, import_errors4.serializeFailure)(err)
4601
4948
  });
4602
4949
  write({ type: "done" });
4603
4950
  finished = true;
@@ -4665,12 +5012,16 @@ async function tryPrepareResumeStream({
4665
5012
  if (!store) return null;
4666
5013
  const pending = await store.get(meta.token);
4667
5014
  if (!pending || pending.requestId !== meta.requestId) {
4668
- throw new Error(
5015
+ throw new ExecutionRequestError(
5016
+ "INVALID_RESUME",
4669
5017
  "Interrupt is expired, already consumed, or does not match the request."
4670
5018
  );
4671
5019
  }
4672
5020
  if (pending.sessionId && pending.sessionId !== sessionId)
4673
- throw new Error("Interrupt belongs to another session.");
5021
+ throw new ExecutionRequestError(
5022
+ "RESUME_MISMATCH",
5023
+ "Interrupt belongs to another session."
5024
+ );
4674
5025
  await validatePending?.(pending);
4675
5026
  const isLimitPause = Boolean(pending.schema.meta?.__kortyxExecutionLimit);
4676
5027
  const savedBudget = pending.state?.config?.executionBudget;
@@ -4690,7 +5041,10 @@ async function tryPrepareResumeStream({
4690
5041
  })();
4691
5042
  }
4692
5043
  if (store.take && !await store.take(meta.token))
4693
- throw new Error("Interrupt has already been resumed or cancelled.");
5044
+ throw new ExecutionRequestError(
5045
+ "INVALID_RESUME",
5046
+ "Interrupt has already been resumed or cancelled."
5047
+ );
4694
5048
  if (meta.cancel) {
4695
5049
  await store.delete(pending.token);
4696
5050
  emitTelemetryEvent({
@@ -4878,10 +5232,7 @@ function resultFromOutcome(outcome, runId, sessionId) {
4878
5232
  return {
4879
5233
  ...info,
4880
5234
  status: "failed",
4881
- error: {
4882
- code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "EXECUTION_FAILED",
4883
- message: error instanceof Error ? error.message : String(error)
4884
- }
5235
+ error: (0, import_errors5.serializeFailure)(error)
4885
5236
  };
4886
5237
  }
4887
5238
  if (outcome.cancelled)
@@ -5055,10 +5406,11 @@ async function resumeWorkflow(services, args) {
5055
5406
  );
5056
5407
  }).catch(
5057
5408
  (error) => reject(
5058
- error instanceof ExecutionRequestError ? error : new ExecutionRequestError(
5059
- "INVALID_RESUME",
5060
- error instanceof Error ? error.message : String(error)
5061
- )
5409
+ error instanceof Error ? error : new import_errors5.KortyxError("RESUME_FAILED", "Resume failed.", {
5410
+ category: "persistence",
5411
+ cause: error,
5412
+ safeMessage: "The saved execution could not be resumed."
5413
+ })
5062
5414
  )
5063
5415
  );
5064
5416
  });
@@ -5662,6 +6014,7 @@ function createAgent(args) {
5662
6014
  createAgent,
5663
6015
  createChatRouteHandler,
5664
6016
  createCheckpointRouteHandler,
6017
+ createFailureResponse,
5665
6018
  extractLatestUserMessage,
5666
6019
  handleChatRequestBody,
5667
6020
  handleCheckpointRequestBody,
@@ -5670,6 +6023,7 @@ function createAgent(args) {
5670
6023
  parseCheckpointRequestBody,
5671
6024
  parseResumeMeta,
5672
6025
  projectWorkflowTopology,
6026
+ readRequestJson,
5673
6027
  streamChat,
5674
6028
  streamChatFromRoute,
5675
6029
  transformGraphStreamForUI,