@kortyx/hooks 0.34.2 → 0.34.3
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 +12 -0
- package/README.md +2 -0
- package/dist/index.js +117 -56
- package/dist/index.js.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/internal.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Return recoverable, bounded model feedback for invalid named interrupt requests. Preserve strict human responses, request quotas, approval gates, and tool-loop limits.
|
|
6
|
+
- Replace child execution snapshots natively while reading legacy object and single-element array snapshots.
|
|
7
|
+
|
|
8
|
+
## [0.34.3](https://github.com/kortyx-io/kortyx/compare/hooks-v0.34.2...hooks-v0.34.3) (2026-10-14)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **runtime:** preserve interrupt state across pauses and resume ([#348](https://github.com/kortyx-io/kortyx/issues/348)) ([03b49f2](https://github.com/kortyx-io/kortyx/commit/03b49f2b8b6b44a2852d16fd4fe1b3e199316072))
|
|
14
|
+
|
|
3
15
|
## [0.34.2](https://github.com/kortyx-io/kortyx/compare/hooks-v0.34.1...hooks-v0.34.2) (2026-10-13)
|
|
4
16
|
|
|
5
17
|
|
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ tool loop, so tools can run before and after a pause. Use `mode: "optional"`
|
|
|
35
35
|
when the model may finish without asking, and `maxRequests` to bound sequential
|
|
36
36
|
human turns.
|
|
37
37
|
|
|
38
|
+
Malformed model-authored requests return an `isError` tool result so the model can correct its arguments. They consume the existing model/loop allowance, but do not issue a human request or consume `maxRequests`. Required mode still requires a valid request. Human response validation and approval denial remain strict.
|
|
39
|
+
|
|
38
40
|
The singular `useReason({ interrupt })` option and `result.interruptResponse`
|
|
39
41
|
are deprecated and will be removed in the next major release. Migrate to
|
|
40
42
|
`interrupts.contracts` and `result.interruptHistory`.
|
package/dist/index.js
CHANGED
|
@@ -245,7 +245,7 @@ var import_node = require("@kortyx/usage-telemetry/node");
|
|
|
245
245
|
// package.json
|
|
246
246
|
var package_default = {
|
|
247
247
|
name: "@kortyx/hooks",
|
|
248
|
-
version: "0.34.
|
|
248
|
+
version: "0.34.3",
|
|
249
249
|
private: false,
|
|
250
250
|
description: "Lifecycle hooks and extension points for Kortyx.",
|
|
251
251
|
keywords: [
|
|
@@ -2965,6 +2965,40 @@ var emitReasonStructuredOutput = (args) => {
|
|
|
2965
2965
|
var import_core5 = require("@kortyx/core");
|
|
2966
2966
|
var import_errors6 = require("@kortyx/core/errors");
|
|
2967
2967
|
var import_zod5 = require("zod");
|
|
2968
|
+
|
|
2969
|
+
// src/reason/input-feedback.ts
|
|
2970
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2971
|
+
function schemaPath(schema, path) {
|
|
2972
|
+
let current = schema;
|
|
2973
|
+
let label = "";
|
|
2974
|
+
for (const part of path.slice(0, 8)) {
|
|
2975
|
+
if (!record(current)) break;
|
|
2976
|
+
if (typeof part === "number" && Number.isSafeInteger(part) && part >= 0) {
|
|
2977
|
+
if (!record(current.items)) break;
|
|
2978
|
+
current = current.items;
|
|
2979
|
+
label += `[${part}]`;
|
|
2980
|
+
} else if (typeof part === "string" && record(current.properties) && Object.hasOwn(current.properties, part)) {
|
|
2981
|
+
current = current.properties[part];
|
|
2982
|
+
label += `${label ? "." : ""}${part.slice(0, 64)}`;
|
|
2983
|
+
} else break;
|
|
2984
|
+
}
|
|
2985
|
+
return label || "input";
|
|
2986
|
+
}
|
|
2987
|
+
function interruptInputFeedback(error, schema, name, required) {
|
|
2988
|
+
const issues = Array.isArray(error.issues) ? error.issues : [];
|
|
2989
|
+
const problems = issues.slice(0, 5).map((issue) => {
|
|
2990
|
+
const path = record(issue) && Array.isArray(issue.path) ? issue.path : [];
|
|
2991
|
+
return `- ${schemaPath(schema, path)} must satisfy its declared input schema.`;
|
|
2992
|
+
});
|
|
2993
|
+
return [
|
|
2994
|
+
`Invalid input for ${name}:`,
|
|
2995
|
+
...problems,
|
|
2996
|
+
...issues.length > 5 ? ["- Additional input fields are invalid."] : [],
|
|
2997
|
+
required ? "Correct the input and call the tool again. Human input is required before finishing." : "Correct the input and call the tool again, or finish without requesting human input when none is needed."
|
|
2998
|
+
].join("\n");
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// src/reason/tools.ts
|
|
2968
3002
|
var toToolDefinitions = (tools2) => tools2.map((tool) => ({
|
|
2969
3003
|
name: tool.name,
|
|
2970
3004
|
...tool.title ? { title: tool.title } : {},
|
|
@@ -3691,11 +3725,35 @@ var runReasonToolLoop = async (args) => {
|
|
|
3691
3725
|
throw new Error(
|
|
3692
3726
|
`useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
|
|
3693
3727
|
);
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3728
|
+
let request;
|
|
3729
|
+
try {
|
|
3730
|
+
request = parseWithSchema(
|
|
3731
|
+
resolved.contract.requestSchema,
|
|
3732
|
+
toolCall.input,
|
|
3733
|
+
`useReason interrupt contract "${resolved.name}" request`
|
|
3734
|
+
);
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
if (!(error instanceof import_errors6.ValidationError)) throw error;
|
|
3737
|
+
const result3 = {
|
|
3738
|
+
toolCallId: toolCall.id,
|
|
3739
|
+
name: toolCall.name,
|
|
3740
|
+
isError: true,
|
|
3741
|
+
content: interruptInputFeedback(
|
|
3742
|
+
error,
|
|
3743
|
+
interruptDefinitions.find((tool2) => tool2.name === toolCall.name)?.inputSchema,
|
|
3744
|
+
toolCall.name,
|
|
3745
|
+
interruptConfig?.mode === "required"
|
|
3746
|
+
)
|
|
3747
|
+
};
|
|
3748
|
+
toolResults.push(result3);
|
|
3749
|
+
messages.push({ role: "tool", ...result3 });
|
|
3750
|
+
traceSpan?.addEvent?.("useReason.interrupt.invalid", {
|
|
3751
|
+
checkpointKey,
|
|
3752
|
+
contract: resolved.name
|
|
3753
|
+
});
|
|
3754
|
+
save();
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3699
3757
|
traceSpan?.addEvent?.("useReason.interrupt.requested", {
|
|
3700
3758
|
checkpointKey,
|
|
3701
3759
|
contract: resolved.name,
|
|
@@ -4635,10 +4693,10 @@ function parallel(values) {
|
|
|
4635
4693
|
throw new Error("Parallel call membership changed during replay.");
|
|
4636
4694
|
}
|
|
4637
4695
|
for (const callId of group.record.calls) {
|
|
4638
|
-
const
|
|
4639
|
-
if (
|
|
4640
|
-
const index =
|
|
4641
|
-
if (
|
|
4696
|
+
const record2 = callRecord(callId);
|
|
4697
|
+
if (record2?.status !== "interrupted") continue;
|
|
4698
|
+
const index = record2.interrupts.length - 1;
|
|
4699
|
+
if (record2.interrupts[index]?.response !== void 0) continue;
|
|
4642
4700
|
group.record.interrupts.push({ callId, index });
|
|
4643
4701
|
await bridge(callId, index);
|
|
4644
4702
|
throw new Error(
|
|
@@ -4809,26 +4867,26 @@ async function runWorkflowCall(args, task) {
|
|
|
4809
4867
|
version: definition?.version ?? null
|
|
4810
4868
|
});
|
|
4811
4869
|
const key = `__useWorkflow:${args.id}`;
|
|
4812
|
-
let
|
|
4813
|
-
if (
|
|
4870
|
+
let record2 = ctx.currentNodeState.byKey[key];
|
|
4871
|
+
if (record2 && record2.parallelKey !== task.group?.key)
|
|
4814
4872
|
throw new WorkflowCallError(
|
|
4815
4873
|
workflow,
|
|
4816
4874
|
"parallel call membership changed during replay"
|
|
4817
4875
|
);
|
|
4818
|
-
if (
|
|
4876
|
+
if (record2 && record2.fingerprint !== fingerprint)
|
|
4819
4877
|
throw new WorkflowCallError(
|
|
4820
4878
|
workflow,
|
|
4821
4879
|
"call input or target changed during replay"
|
|
4822
4880
|
);
|
|
4823
|
-
if (!
|
|
4824
|
-
|
|
4881
|
+
if (!record2) ctx.node.consumeExecution?.("maxChildInvocations");
|
|
4882
|
+
record2 ??= {
|
|
4825
4883
|
fingerprint,
|
|
4826
4884
|
invocationId: (0, import_node_crypto6.randomUUID)(),
|
|
4827
4885
|
status: "running",
|
|
4828
4886
|
interrupts: [],
|
|
4829
4887
|
...task.group ? { parallelKey: task.group.key } : {}
|
|
4830
4888
|
};
|
|
4831
|
-
ctx.currentNodeState.byKey[key] =
|
|
4889
|
+
ctx.currentNodeState.byKey[key] = record2;
|
|
4832
4890
|
ctx.stateDirty = true;
|
|
4833
4891
|
if (!task.group) ctx.workflowCallActive = true;
|
|
4834
4892
|
const telemetry = ctx.node.workflowCallTelemetry;
|
|
@@ -4836,7 +4894,7 @@ async function runWorkflowCall(args, task) {
|
|
|
4836
4894
|
ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto6.randomUUID)();
|
|
4837
4895
|
const callerNodeExecutionId = ctx.currentNodeState.byKey[activationKey];
|
|
4838
4896
|
const metadata = {
|
|
4839
|
-
invocationId:
|
|
4897
|
+
invocationId: record2.invocationId,
|
|
4840
4898
|
parentInvocationId: telemetry?.correlation?.invocationId ?? null,
|
|
4841
4899
|
branchId: telemetry?.correlation?.branchId ?? telemetry?.correlation?.runId ?? "unknown",
|
|
4842
4900
|
callId: args.id,
|
|
@@ -4846,46 +4904,49 @@ async function runWorkflowCall(args, task) {
|
|
|
4846
4904
|
targetWorkflowId: workflow,
|
|
4847
4905
|
targetVersion: definition?.version ?? null
|
|
4848
4906
|
};
|
|
4849
|
-
|
|
4907
|
+
record2.telemetry = metadata;
|
|
4850
4908
|
const report = (status, content = {}) => {
|
|
4851
|
-
|
|
4909
|
+
record2.sequence = (record2.sequence ?? 0) + 1;
|
|
4852
4910
|
emitWorkflowCall(telemetry, `workflow.call.${status}`, {
|
|
4853
4911
|
...metadata,
|
|
4854
|
-
sequence:
|
|
4912
|
+
sequence: record2.sequence,
|
|
4855
4913
|
...content
|
|
4856
4914
|
});
|
|
4857
4915
|
};
|
|
4858
4916
|
const recordUsage = (usage2) => {
|
|
4859
4917
|
if (!usage2) return;
|
|
4860
4918
|
accumulateTokenUsage({
|
|
4861
|
-
input: usage2.input - (
|
|
4862
|
-
output: usage2.output - (
|
|
4863
|
-
total: usage2.total - (
|
|
4919
|
+
input: usage2.input - (record2.usage?.input ?? 0),
|
|
4920
|
+
output: usage2.output - (record2.usage?.output ?? 0),
|
|
4921
|
+
total: usage2.total - (record2.usage?.total ?? 0)
|
|
4864
4922
|
});
|
|
4865
|
-
|
|
4923
|
+
record2.usage = usage2;
|
|
4866
4924
|
};
|
|
4867
|
-
const cached =
|
|
4925
|
+
const cached = record2.status === "completed";
|
|
4868
4926
|
try {
|
|
4869
|
-
for (const interruption of task.group ? [] :
|
|
4927
|
+
for (const interruption of task.group ? [] : record2.interrupts) {
|
|
4870
4928
|
interruption.response = await awaitInterruptInternal({
|
|
4871
4929
|
request: interruption.request,
|
|
4872
4930
|
...interruption.request.meta ? { meta: interruption.request.meta } : {}
|
|
4873
4931
|
});
|
|
4874
4932
|
}
|
|
4875
|
-
if (
|
|
4933
|
+
if (record2.status === "failed")
|
|
4876
4934
|
throw new WorkflowCallError(
|
|
4877
4935
|
workflow,
|
|
4878
|
-
|
|
4936
|
+
record2.error ?? "child failed",
|
|
4879
4937
|
void 0,
|
|
4880
|
-
|
|
4938
|
+
record2.failure ?? (0, import_errors8.serializeFailure)(void 0)
|
|
4881
4939
|
);
|
|
4882
|
-
if (task.group &&
|
|
4940
|
+
if (task.group && record2.status === "interrupted" && record2.interrupts.at(-1)?.response === void 0)
|
|
4883
4941
|
throw new ParallelChildWaiting();
|
|
4884
|
-
while (
|
|
4942
|
+
while (record2.status !== "completed") {
|
|
4885
4943
|
let outcome;
|
|
4886
4944
|
try {
|
|
4945
|
+
if (Array.isArray(record2.snapshot) && record2.snapshot.length !== 1)
|
|
4946
|
+
throw new import_errors8.WorkflowContractError("Invalid child workflow snapshot.");
|
|
4947
|
+
const snapshot = Array.isArray(record2.snapshot) ? record2.snapshot[0] : record2.snapshot;
|
|
4887
4948
|
report(
|
|
4888
|
-
|
|
4949
|
+
snapshot ? "resumed" : "started",
|
|
4889
4950
|
workflowCallContent(telemetry, "input", input)
|
|
4890
4951
|
);
|
|
4891
4952
|
outcome = await service({
|
|
@@ -4894,9 +4955,9 @@ async function runWorkflowCall(args, task) {
|
|
|
4894
4955
|
workflow,
|
|
4895
4956
|
input,
|
|
4896
4957
|
definition,
|
|
4897
|
-
invocationId:
|
|
4898
|
-
...
|
|
4899
|
-
...
|
|
4958
|
+
invocationId: record2.invocationId,
|
|
4959
|
+
...snapshot ? { snapshot } : {},
|
|
4960
|
+
...record2.interrupts.length ? { response: record2.interrupts.at(-1)?.response } : {}
|
|
4900
4961
|
});
|
|
4901
4962
|
} catch (error) {
|
|
4902
4963
|
const child = error;
|
|
@@ -4905,48 +4966,48 @@ async function runWorkflowCall(args, task) {
|
|
|
4905
4966
|
if ((0, import_core8.isExecutionCancelled)(error)) throw error;
|
|
4906
4967
|
if ((0, import_core8.isExecutionLimitReached)(error)) {
|
|
4907
4968
|
if (child?.__kortyxChildSnapshot)
|
|
4908
|
-
|
|
4969
|
+
record2.snapshot = child.__kortyxChildSnapshot;
|
|
4909
4970
|
report("suspended", {
|
|
4910
4971
|
reason: "limit_reached",
|
|
4911
4972
|
limit: error.limitReached
|
|
4912
4973
|
});
|
|
4913
4974
|
throw error;
|
|
4914
4975
|
}
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4976
|
+
record2.status = "failed";
|
|
4977
|
+
record2.failure = (0, import_errors8.serializeFailure)(error);
|
|
4978
|
+
record2.error = record2.failure.message;
|
|
4918
4979
|
report("failed", { error: "Child workflow failed" });
|
|
4919
4980
|
throw new WorkflowCallError(
|
|
4920
4981
|
workflow,
|
|
4921
|
-
|
|
4982
|
+
record2.error,
|
|
4922
4983
|
error,
|
|
4923
|
-
|
|
4984
|
+
record2.failure
|
|
4924
4985
|
);
|
|
4925
4986
|
}
|
|
4926
4987
|
(0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
|
|
4927
4988
|
recordUsage(outcome.usage);
|
|
4928
4989
|
if (outcome.status === "completed") {
|
|
4929
|
-
|
|
4930
|
-
workflowCallFingerprint(
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
for (const observation of
|
|
4990
|
+
record2.data = outcome.data;
|
|
4991
|
+
workflowCallFingerprint(record2.data);
|
|
4992
|
+
record2.status = "completed";
|
|
4993
|
+
record2.toolObservations = outcome.toolObservations ?? [];
|
|
4994
|
+
for (const observation of record2.toolObservations)
|
|
4934
4995
|
rememberToolObservation(observation);
|
|
4935
|
-
delete
|
|
4996
|
+
delete record2.snapshot;
|
|
4936
4997
|
report(
|
|
4937
4998
|
"completed",
|
|
4938
|
-
workflowCallContent(telemetry, "output",
|
|
4999
|
+
workflowCallContent(telemetry, "output", record2.data)
|
|
4939
5000
|
);
|
|
4940
5001
|
} else {
|
|
4941
|
-
|
|
4942
|
-
|
|
5002
|
+
record2.status = "interrupted";
|
|
5003
|
+
record2.snapshot = outcome.snapshot;
|
|
4943
5004
|
report("suspended", {
|
|
4944
5005
|
leaf: outcome.request.meta?.workflowCall ?? null
|
|
4945
5006
|
});
|
|
4946
5007
|
const interruption = {
|
|
4947
5008
|
request: outcome.request
|
|
4948
5009
|
};
|
|
4949
|
-
|
|
5010
|
+
record2.interrupts.push(interruption);
|
|
4950
5011
|
if (task.group) throw new ParallelChildWaiting();
|
|
4951
5012
|
interruption.response = await awaitInterruptInternal({
|
|
4952
5013
|
request: interruption.request,
|
|
@@ -4955,12 +5016,12 @@ async function runWorkflowCall(args, task) {
|
|
|
4955
5016
|
}
|
|
4956
5017
|
}
|
|
4957
5018
|
if (cached) {
|
|
4958
|
-
report("reused", workflowCallContent(telemetry, "output",
|
|
4959
|
-
if (
|
|
4960
|
-
await replayToolObservations(
|
|
4961
|
-
delete
|
|
5019
|
+
report("reused", workflowCallContent(telemetry, "output", record2.data));
|
|
5020
|
+
if (record2.restoredToolBranch !== telemetry?.correlation?.branchId)
|
|
5021
|
+
await replayToolObservations(record2.toolObservations ?? [], true);
|
|
5022
|
+
delete record2.restoredToolBranch;
|
|
4962
5023
|
}
|
|
4963
|
-
const data =
|
|
5024
|
+
const data = record2.data ?? {};
|
|
4964
5025
|
return {
|
|
4965
5026
|
data: JSON.parse(JSON.stringify(data))
|
|
4966
5027
|
};
|