@kortyx/hooks 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +10 -3
- package/dist/index.d.ts +47 -6
- package/dist/index.js +288 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.28.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.27.0...hooks-v0.28.0) (2026-09-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **hooks:** add model-driven interrupt contracts to useReason ([#224](https://github.com/kortyx-io/kortyx/issues/224)) ([e0147d5](https://github.com/kortyx-io/kortyx/commit/e0147d5234ca38d450d452af924b242f8cbdeb16))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @kortyx/core bumped to 0.10.0
|
|
16
|
+
* @kortyx/providers bumped to 0.8.1
|
|
17
|
+
* @kortyx/telemetry-contracts bumped to 0.11.0
|
|
18
|
+
|
|
3
19
|
## [0.27.0](https://github.com/kortyx-io/kortyx/compare/hooks-v0.26.0...hooks-v0.27.0) (2026-09-21)
|
|
4
20
|
|
|
5
21
|
|
package/README.md
CHANGED
|
@@ -27,9 +27,16 @@ npm install @kortyx/hooks
|
|
|
27
27
|
- `useRuntimeContext(...)` for request context made available to node execution.
|
|
28
28
|
- `useNodeState(...)` / `useWorkflowState(...)` for stateful node logic.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
`
|
|
32
|
-
|
|
30
|
+
Define reusable model-driven human-input contracts with
|
|
31
|
+
`defineInterruptContract(...)`, then pass one or more through
|
|
32
|
+
`useReason({ interrupts: { contracts } })`. Contract calls share the ordinary
|
|
33
|
+
tool loop, so tools can run before and after a pause. Use `mode: "optional"`
|
|
34
|
+
when the model may finish without asking, and `maxRequests` to bound sequential
|
|
35
|
+
human turns.
|
|
36
|
+
|
|
37
|
+
The singular `useReason({ interrupt })` option and `result.interruptResponse`
|
|
38
|
+
are deprecated and will be removed in the next major release. Migrate to
|
|
39
|
+
`interrupts.contracts` and `result.interruptHistory`.
|
|
33
40
|
|
|
34
41
|
## Runtime Resume Behavior
|
|
35
42
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _kortyx_core from '@kortyx/core';
|
|
2
|
-
import { WorkflowDefinition, TokenUsage, InterruptInput, GraphState, NodeConfig, InterruptResult } from '@kortyx/core';
|
|
2
|
+
import { WorkflowDefinition, TokenUsage, InterruptInput, GraphState, NodeConfig, InterruptResumeValue, InterruptResult } from '@kortyx/core';
|
|
3
3
|
import { K as KortyxTelemetryConfig, R as ReasonTraceAdapter, a as KortyxTraceMetadata, b as ReportErrorOptions, c as KortyxTelemetryEventType } from './tracing-BsxYlgEw.js';
|
|
4
4
|
export { E as EnsureWorkflowTopologyRequest, d as EnsureWorkflowTopologyResponse, e as KortyxErrorDetails, f as KortyxTelemetryContentCapture, g as KortyxTelemetryCorrelation, h as KortyxTelemetryEvent, i as KortyxTelemetryPrompt, j as KortyxTelemetryReporter, k as KortyxTelemetryService, l as KortyxTraceAdapter, m as KortyxTraceErrorProjection, n as ReasonTraceAttributes, o as ReasonTraceSpan, p as ReasonTraceSpanEndArgs, q as ReasonTraceSpanStartArgs } from './tracing-BsxYlgEw.js';
|
|
5
5
|
import { FailureDescriptor, KortyxError } from '@kortyx/core/errors';
|
|
@@ -95,7 +95,7 @@ type HookNodeRuntimeContext = {
|
|
|
95
95
|
};
|
|
96
96
|
config: NodeConfig;
|
|
97
97
|
emit: (event: string, payload: unknown) => void;
|
|
98
|
-
awaitInterrupt: (args: InterruptInput) =>
|
|
98
|
+
awaitInterrupt: (args: InterruptInput) => InterruptResumeValue;
|
|
99
99
|
callWorkflow?: WorkflowCallService;
|
|
100
100
|
workflowCallTelemetry?: KortyxTelemetryConfig | undefined;
|
|
101
101
|
};
|
|
@@ -120,6 +120,30 @@ type SchemaLike<T> = {
|
|
|
120
120
|
};
|
|
121
121
|
};
|
|
122
122
|
};
|
|
123
|
+
type InterruptContract<TRequest = unknown, TResponse = unknown> = {
|
|
124
|
+
/** Human-readable guidance included in the model-visible control tool. */
|
|
125
|
+
description: string;
|
|
126
|
+
requestSchema: SchemaLike<TRequest>;
|
|
127
|
+
responseSchema: SchemaLike<TResponse>;
|
|
128
|
+
/** Stable UI contract identifier. */
|
|
129
|
+
schemaId: string;
|
|
130
|
+
/** Version of the UI contract payload. */
|
|
131
|
+
schemaVersion: string;
|
|
132
|
+
};
|
|
133
|
+
type InterruptContractMap = Record<string, InterruptContract<unknown, unknown>>;
|
|
134
|
+
type UseReasonInterruptsConfig<TContracts extends InterruptContractMap = InterruptContractMap> = {
|
|
135
|
+
mode?: "required" | "optional" | undefined;
|
|
136
|
+
/** Maximum number of model-requested human interactions for this operation. */
|
|
137
|
+
maxRequests?: number | undefined;
|
|
138
|
+
contracts: TContracts;
|
|
139
|
+
};
|
|
140
|
+
type InterruptHistoryEntry<TContracts extends InterruptContractMap = InterruptContractMap> = {
|
|
141
|
+
[TName in Extract<keyof TContracts, string>]: TContracts[TName] extends InterruptContract<infer TRequest, infer TResponse> ? {
|
|
142
|
+
contract: TName;
|
|
143
|
+
request: TRequest;
|
|
144
|
+
response: TResponse;
|
|
145
|
+
} : never;
|
|
146
|
+
}[Extract<keyof TContracts, string>];
|
|
123
147
|
type StructuredPath = string;
|
|
124
148
|
type UseReasonStructuredFieldMode = "append" | "text-delta" | "set";
|
|
125
149
|
type UseReasonStructuredFieldKey = string;
|
|
@@ -171,6 +195,10 @@ type UseReasonStructuredConfig = {
|
|
|
171
195
|
*/
|
|
172
196
|
fields?: UseReasonStructuredFields | undefined;
|
|
173
197
|
};
|
|
198
|
+
/**
|
|
199
|
+
* @deprecated Use `UseReasonInterruptsConfig` and `defineInterruptContract`.
|
|
200
|
+
* Removed in the next major release.
|
|
201
|
+
*/
|
|
174
202
|
type UseReasonInterruptConfig<TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult> = {
|
|
175
203
|
mode?: "required" | "optional" | undefined;
|
|
176
204
|
requestSchema: SchemaLike<TRequest>;
|
|
@@ -183,7 +211,7 @@ type UseReasonToolExecution = {
|
|
|
183
211
|
approval?: boolean | Record<string, boolean> | undefined;
|
|
184
212
|
emit?: boolean | Record<string, boolean> | undefined;
|
|
185
213
|
};
|
|
186
|
-
type UseReasonArgs<TOutput = unknown, TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult> = {
|
|
214
|
+
type UseReasonArgs<TOutput = unknown, TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult, TContracts extends InterruptContractMap = InterruptContractMap> = {
|
|
187
215
|
model: ProviderModelRef;
|
|
188
216
|
input: string;
|
|
189
217
|
system?: string | undefined;
|
|
@@ -200,7 +228,9 @@ type UseReasonArgs<TOutput = unknown, TRequest extends InterruptInput = Interrup
|
|
|
200
228
|
telemetry?: KortyxTraceMetadata | undefined;
|
|
201
229
|
outputSchema?: SchemaLike<TOutput> | undefined;
|
|
202
230
|
structured?: UseReasonStructuredConfig | undefined;
|
|
231
|
+
/** @deprecated Use `interrupts`. Removed in the next major release. */
|
|
203
232
|
interrupt?: UseReasonInterruptConfig<TRequest, TResponse> | undefined;
|
|
233
|
+
interrupts?: UseReasonInterruptsConfig<TContracts> | undefined;
|
|
204
234
|
tools?: KortyxExecutableTool[] | undefined;
|
|
205
235
|
toolExecution?: UseReasonToolExecution | undefined;
|
|
206
236
|
};
|
|
@@ -215,7 +245,7 @@ type UseReasonStep = {
|
|
|
215
245
|
providerMetadata?: KortyxProviderMetadata | undefined;
|
|
216
246
|
warnings?: KortyxWarning[] | undefined;
|
|
217
247
|
};
|
|
218
|
-
type UseReasonResult<TOutput = unknown, TResponse = InterruptResult> = {
|
|
248
|
+
type UseReasonResult<TOutput = unknown, TResponse = InterruptResult, TContracts extends InterruptContractMap = InterruptContractMap> = {
|
|
219
249
|
id?: string;
|
|
220
250
|
opId: string;
|
|
221
251
|
text: string;
|
|
@@ -225,7 +255,9 @@ type UseReasonResult<TOutput = unknown, TResponse = InterruptResult> = {
|
|
|
225
255
|
providerMetadata?: KortyxProviderMetadata;
|
|
226
256
|
warnings?: KortyxWarning[];
|
|
227
257
|
output?: TOutput;
|
|
258
|
+
/** @deprecated Use `interruptHistory`. Removed in the next major release. */
|
|
228
259
|
interruptResponse?: TResponse;
|
|
260
|
+
interruptHistory?: InterruptHistoryEntry<TContracts>[];
|
|
229
261
|
toolCalls?: KortyxToolCall[];
|
|
230
262
|
toolResults?: KortyxToolResult[];
|
|
231
263
|
steps?: UseReasonStep[];
|
|
@@ -239,10 +271,19 @@ type UseInterruptArgs<TRequest extends InterruptInput = InterruptInput, TRespons
|
|
|
239
271
|
id?: string | undefined;
|
|
240
272
|
meta?: Record<string, unknown> | undefined;
|
|
241
273
|
};
|
|
274
|
+
type UseContractInterruptArgs<TRequest, TResponse> = {
|
|
275
|
+
contract: InterruptContract<TRequest, TResponse>;
|
|
276
|
+
request: TRequest;
|
|
277
|
+
id?: string | undefined;
|
|
278
|
+
meta?: Record<string, unknown> | undefined;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
declare const defineInterruptContract: <TRequest, TResponse>(contract: InterruptContract<TRequest, TResponse>) => InterruptContract<TRequest, TResponse>;
|
|
242
282
|
|
|
243
283
|
type StateSetter<T> = (next: T | ((prev: T) => T)) => void;
|
|
244
284
|
declare function useStructuredData<TData = unknown>(args: UseStructuredDataArgs<TData>): void;
|
|
245
|
-
declare function useReason<TOutput = unknown, TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult>(args: UseReasonArgs<TOutput, TRequest, TResponse>): Promise<UseReasonResult<TOutput, TResponse>>;
|
|
285
|
+
declare function useReason<TOutput = unknown, TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult, TContracts extends InterruptContractMap = InterruptContractMap>(args: UseReasonArgs<TOutput, TRequest, TResponse, TContracts>): Promise<UseReasonResult<TOutput, TResponse, TContracts>>;
|
|
286
|
+
declare function useInterrupt<TRequest, TResponse>(args: UseContractInterruptArgs<TRequest, TResponse>): Promise<TResponse>;
|
|
246
287
|
declare function useInterrupt<TRequest extends InterruptInput = InterruptInput, TResponse = InterruptResult>(args: UseInterruptArgs<TRequest, TResponse>): Promise<TResponse>;
|
|
247
288
|
/** Live execution signal for cooperative I/O. Never store it in workflow state. */
|
|
248
289
|
declare function useAbortSignal(): AbortSignal | undefined;
|
|
@@ -264,4 +305,4 @@ declare function useTool<T extends KortyxExecutableTool>(args: UseToolArgs<T>):
|
|
|
264
305
|
/** Best effort lifecycle facts. Engine snapshots and interrupt tokens never leave the runtime. */
|
|
265
306
|
declare function emitWorkflowCall(telemetry: KortyxTelemetryConfig | undefined, type: KortyxTelemetryEventType, payload: Record<string, unknown>): void;
|
|
266
307
|
|
|
267
|
-
export { type CompleteResponseOptions, type HookRuntimeContext, KortyxTelemetryConfig, KortyxTelemetryEventType, KortyxTraceMetadata, ParallelError, ReasonTraceAdapter, ReportErrorOptions, type UseInterruptArgs, type UseReasonArgs, type UseReasonResult, type UseReasonStep, type UseReasonToolExecution, type UseStructuredDataArgs, type UseToolArgs, WorkflowCallError, type WorkflowCallOutcome, type WorkflowCallService, completeResponse, createWorkflowHooks, emitWorkflowCall, parallel, reportError, runWithHookContext, useAbortSignal, useInterrupt, useNodeState, useReason, useRuntimeContext, useStructuredData, useTool, useWorkflow, useWorkflowState, workflowCallFingerprint };
|
|
308
|
+
export { type CompleteResponseOptions, type HookRuntimeContext, type InterruptContract, type InterruptContractMap, type InterruptHistoryEntry, KortyxTelemetryConfig, KortyxTelemetryEventType, KortyxTraceMetadata, ParallelError, ReasonTraceAdapter, ReportErrorOptions, type UseContractInterruptArgs, type UseInterruptArgs, type UseReasonArgs, type UseReasonInterruptConfig, type UseReasonInterruptsConfig, type UseReasonResult, type UseReasonStep, type UseReasonToolExecution, type UseStructuredDataArgs, type UseToolArgs, WorkflowCallError, type WorkflowCallOutcome, type WorkflowCallService, completeResponse, createWorkflowHooks, defineInterruptContract, emitWorkflowCall, parallel, reportError, runWithHookContext, useAbortSignal, useInterrupt, useNodeState, useReason, useRuntimeContext, useStructuredData, useTool, useWorkflow, useWorkflowState, workflowCallFingerprint };
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
WorkflowCallError: () => WorkflowCallError,
|
|
24
24
|
completeResponse: () => completeResponse,
|
|
25
25
|
createWorkflowHooks: () => createWorkflowHooks,
|
|
26
|
+
defineInterruptContract: () => defineInterruptContract,
|
|
26
27
|
emitWorkflowCall: () => emitWorkflowCall,
|
|
27
28
|
parallel: () => parallel,
|
|
28
29
|
reportError: () => reportError,
|
|
@@ -349,6 +350,97 @@ var awaitInterruptInternal = (args) => {
|
|
|
349
350
|
return Promise.resolve(response);
|
|
350
351
|
};
|
|
351
352
|
|
|
353
|
+
// src/interrupt-contract.ts
|
|
354
|
+
var CONTROL_TOOL_PREFIX = "kortyx_request_input__";
|
|
355
|
+
var LEGACY_CONTRACT_NAME = "default";
|
|
356
|
+
var legacyWarningEmitted = false;
|
|
357
|
+
var legacyConfigs = /* @__PURE__ */ new WeakSet();
|
|
358
|
+
var defineInterruptContract = (contract) => {
|
|
359
|
+
if (!contract || typeof contract !== "object")
|
|
360
|
+
throw new Error("defineInterruptContract requires a contract object.");
|
|
361
|
+
for (const [field, value] of [
|
|
362
|
+
["description", contract.description],
|
|
363
|
+
["schemaId", contract.schemaId],
|
|
364
|
+
["schemaVersion", contract.schemaVersion]
|
|
365
|
+
])
|
|
366
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
367
|
+
throw new Error(
|
|
368
|
+
`Interrupt contract ${field} must be a non-empty string.`
|
|
369
|
+
);
|
|
370
|
+
if (typeof contract.requestSchema?.safeParse !== "function")
|
|
371
|
+
throw new Error(
|
|
372
|
+
"Interrupt contract requestSchema must provide safeParse(value)."
|
|
373
|
+
);
|
|
374
|
+
if (typeof contract.responseSchema?.safeParse !== "function")
|
|
375
|
+
throw new Error(
|
|
376
|
+
"Interrupt contract responseSchema must provide safeParse(value)."
|
|
377
|
+
);
|
|
378
|
+
return contract;
|
|
379
|
+
};
|
|
380
|
+
var interruptControlToolName = (name) => `${CONTROL_TOOL_PREFIX}${name}`;
|
|
381
|
+
var isInterruptControlToolName = (name) => name.startsWith(CONTROL_TOOL_PREFIX);
|
|
382
|
+
var assertInterruptContractName = (name) => {
|
|
383
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))
|
|
384
|
+
throw new Error(
|
|
385
|
+
`Invalid useReason interrupt contract name "${name}". Use letters, numbers, underscores, or hyphens, starting with a letter.`
|
|
386
|
+
);
|
|
387
|
+
if (interruptControlToolName(name).length > 64)
|
|
388
|
+
throw new Error(
|
|
389
|
+
`useReason interrupt contract name "${name}" is too long for a provider tool name.`
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
var warnLegacyUseReasonInterrupt = () => {
|
|
393
|
+
if (legacyWarningEmitted) return;
|
|
394
|
+
legacyWarningEmitted = true;
|
|
395
|
+
process.emitWarning(
|
|
396
|
+
"useReason({ interrupt }) is deprecated and will be removed in the next major release. Define an interrupt contract with defineInterruptContract() and pass it through useReason({ interrupts: { contracts } }). See docs/internal/next-major-removals.md.",
|
|
397
|
+
{
|
|
398
|
+
code: "KORTYX_USE_REASON_INTERRUPT_DEPRECATED",
|
|
399
|
+
type: "DeprecationWarning"
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
var normalizeReasonInterrupts = (args) => {
|
|
404
|
+
if (args.interrupt && args.interrupts)
|
|
405
|
+
throw new Error(
|
|
406
|
+
"useReason cannot receive both deprecated `interrupt` and `interrupts`. Migrate the legacy configuration to an interrupt contract."
|
|
407
|
+
);
|
|
408
|
+
if (args.interrupts) {
|
|
409
|
+
for (const [name, contract] of Object.entries(args.interrupts.contracts)) {
|
|
410
|
+
assertInterruptContractName(name);
|
|
411
|
+
defineInterruptContract(contract);
|
|
412
|
+
}
|
|
413
|
+
if (Object.keys(args.interrupts.contracts).length === 0)
|
|
414
|
+
throw new Error("useReason interrupts.contracts cannot be empty.");
|
|
415
|
+
if (args.interrupts.maxRequests !== void 0 && (!Number.isInteger(args.interrupts.maxRequests) || args.interrupts.maxRequests < 1))
|
|
416
|
+
throw new Error("useReason interrupts.maxRequests must be at least 1.");
|
|
417
|
+
return args.interrupts;
|
|
418
|
+
}
|
|
419
|
+
if (!args.interrupt) return void 0;
|
|
420
|
+
warnLegacyUseReasonInterrupt();
|
|
421
|
+
const normalized = {
|
|
422
|
+
mode: args.interrupt.mode,
|
|
423
|
+
maxRequests: 1,
|
|
424
|
+
contracts: {
|
|
425
|
+
[LEGACY_CONTRACT_NAME]: {
|
|
426
|
+
description: "Ask the user for the additional information required to complete this request.",
|
|
427
|
+
requestSchema: args.interrupt.requestSchema,
|
|
428
|
+
responseSchema: args.interrupt.responseSchema ?? {
|
|
429
|
+
safeParse: (value) => ({
|
|
430
|
+
success: true,
|
|
431
|
+
data: value
|
|
432
|
+
})
|
|
433
|
+
},
|
|
434
|
+
schemaId: args.interrupt.schemaId ?? "kortyx.legacy-interrupt",
|
|
435
|
+
schemaVersion: args.interrupt.schemaVersion ?? "1"
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
legacyConfigs.add(normalized);
|
|
440
|
+
return normalized;
|
|
441
|
+
};
|
|
442
|
+
var isLegacyInterruptContracts = (config) => Boolean(config && legacyConfigs.has(config));
|
|
443
|
+
|
|
352
444
|
// src/reason/use-reason.ts
|
|
353
445
|
var import_core6 = require("@kortyx/core");
|
|
354
446
|
|
|
@@ -2362,6 +2454,7 @@ var emitReasonStructuredOutput = (args) => {
|
|
|
2362
2454
|
// src/reason/tools.ts
|
|
2363
2455
|
var import_core5 = require("@kortyx/core");
|
|
2364
2456
|
var import_errors6 = require("@kortyx/core/errors");
|
|
2457
|
+
var import_zod4 = require("zod");
|
|
2365
2458
|
var toToolDefinitions = (tools2) => tools2.map((tool) => ({
|
|
2366
2459
|
name: tool.name,
|
|
2367
2460
|
...tool.title ? { title: tool.title } : {},
|
|
@@ -2397,7 +2490,12 @@ var normalizeToolResult = (toolCall, value) => {
|
|
|
2397
2490
|
};
|
|
2398
2491
|
};
|
|
2399
2492
|
var createInitialMessages = (args) => [
|
|
2400
|
-
...typeof args.system === "string"
|
|
2493
|
+
...typeof args.system === "string" || args.interruptInstructions ? [
|
|
2494
|
+
{
|
|
2495
|
+
role: "system",
|
|
2496
|
+
content: [args.system, args.interruptInstructions].filter((value) => Boolean(value)).join("\n\n")
|
|
2497
|
+
}
|
|
2498
|
+
] : [],
|
|
2401
2499
|
{ role: "user", content: String(args.input ?? "") }
|
|
2402
2500
|
];
|
|
2403
2501
|
var runReasonToolLoop = async (args) => {
|
|
@@ -2412,19 +2510,23 @@ var runReasonToolLoop = async (args) => {
|
|
|
2412
2510
|
const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
|
|
2413
2511
|
const opId = checkpoint?.opId ?? args.opId;
|
|
2414
2512
|
const tools2 = useReasonArgs.tools ?? [];
|
|
2513
|
+
const interruptConfig = normalizeReasonInterrupts({
|
|
2514
|
+
interrupt: useReasonArgs.interrupt,
|
|
2515
|
+
interrupts: useReasonArgs.interrupts
|
|
2516
|
+
});
|
|
2517
|
+
const interruptByToolName = /* @__PURE__ */ new Map();
|
|
2415
2518
|
const toolByName = /* @__PURE__ */ new Map();
|
|
2416
2519
|
let separateOutput = false;
|
|
2417
2520
|
for (const tool of tools2) {
|
|
2521
|
+
if (isInterruptControlToolName(tool.name))
|
|
2522
|
+
throw new Error(
|
|
2523
|
+
`useReason tool name "${tool.name}" uses the reserved interrupt control-tool namespace.`
|
|
2524
|
+
);
|
|
2418
2525
|
if (toolByName.has(tool.name)) {
|
|
2419
2526
|
throw new Error(`useReason received duplicate tool name "${tool.name}".`);
|
|
2420
2527
|
}
|
|
2421
2528
|
toolByName.set(tool.name, tool);
|
|
2422
2529
|
}
|
|
2423
|
-
if (useReasonArgs.interrupt) {
|
|
2424
|
-
throw new Error(
|
|
2425
|
-
"useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
|
|
2426
|
-
);
|
|
2427
|
-
}
|
|
2428
2530
|
separateOutput = Boolean(
|
|
2429
2531
|
useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
|
|
2430
2532
|
...useReasonArgs.model.options,
|
|
@@ -2436,21 +2538,63 @@ var runReasonToolLoop = async (args) => {
|
|
|
2436
2538
|
) ?? {}
|
|
2437
2539
|
}).requiresSeparateStructuredOutput
|
|
2438
2540
|
);
|
|
2439
|
-
const
|
|
2541
|
+
const interruptDefinitions = [];
|
|
2542
|
+
for (const [name, contract] of Object.entries(
|
|
2543
|
+
interruptConfig?.contracts ?? {}
|
|
2544
|
+
)) {
|
|
2545
|
+
const toolName = interruptControlToolName(name);
|
|
2546
|
+
if (toolByName.has(toolName))
|
|
2547
|
+
throw new Error(
|
|
2548
|
+
`useReason tool name "${toolName}" is reserved for interrupt contracts.`
|
|
2549
|
+
);
|
|
2550
|
+
let inputSchema;
|
|
2551
|
+
try {
|
|
2552
|
+
inputSchema = (0, import_zod4.toJSONSchema)(contract.requestSchema);
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
throw new Error(
|
|
2555
|
+
`useReason interrupt contract "${name}" requestSchema must support JSON Schema conversion.`,
|
|
2556
|
+
{ cause: error }
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
interruptByToolName.set(toolName, { name, contract });
|
|
2560
|
+
interruptDefinitions.push({
|
|
2561
|
+
name: toolName,
|
|
2562
|
+
title: `Request human input: ${name}`,
|
|
2563
|
+
description: [
|
|
2564
|
+
contract.description,
|
|
2565
|
+
"Call this only when human input is needed. Execution pauses until the validated response is returned as this tool's result."
|
|
2566
|
+
].join(" "),
|
|
2567
|
+
inputSchema,
|
|
2568
|
+
metadata: {
|
|
2569
|
+
kortyxControl: "interrupt",
|
|
2570
|
+
contract: name,
|
|
2571
|
+
schemaId: contract.schemaId,
|
|
2572
|
+
schemaVersion: contract.schemaVersion
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
const toolDefinitions = [
|
|
2577
|
+
...toToolDefinitions(tools2),
|
|
2578
|
+
...interruptDefinitions
|
|
2579
|
+
];
|
|
2440
2580
|
let finalizing = checkpoint?.finalizing ?? false;
|
|
2441
2581
|
let completed = false;
|
|
2442
2582
|
const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
|
|
2443
2583
|
const messages = checkpoint?.messages ?? createInitialMessages({
|
|
2444
2584
|
system: useReasonArgs.system,
|
|
2445
|
-
input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input
|
|
2585
|
+
input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input,
|
|
2586
|
+
...interruptConfig ? {
|
|
2587
|
+
interruptInstructions: [
|
|
2588
|
+
"Human-input rules:",
|
|
2589
|
+
"- The kortyx_request_input__* tools pause this reasoning operation and return the validated human response as a normal tool result.",
|
|
2590
|
+
"- Call at most one human-input tool in a model turn, and do not combine it with other tool calls in that turn.",
|
|
2591
|
+
interruptConfig.mode === "required" ? "- You must call at least one human-input tool before returning the final answer." : "- Human input is optional; call a human-input tool only when the request cannot be completed safely without it."
|
|
2592
|
+
].join("\n")
|
|
2593
|
+
} : {}
|
|
2446
2594
|
});
|
|
2447
2595
|
const steps = checkpoint?.steps ?? [];
|
|
2448
|
-
const allToolCalls = steps.flatMap(
|
|
2449
|
-
|
|
2450
|
-
);
|
|
2451
|
-
const allToolResults = steps.flatMap(
|
|
2452
|
-
(step) => step.toolResults
|
|
2453
|
-
);
|
|
2596
|
+
const allToolCalls = steps.flatMap((step) => step.toolCalls).filter((call) => !interruptByToolName.has(call.name));
|
|
2597
|
+
const allToolResults = steps.flatMap((step) => step.toolResults).filter((result) => !interruptByToolName.has(result.name));
|
|
2454
2598
|
let finalText = "";
|
|
2455
2599
|
let finalRaw;
|
|
2456
2600
|
let finalOutput;
|
|
@@ -2466,6 +2610,7 @@ var runReasonToolLoop = async (args) => {
|
|
|
2466
2610
|
);
|
|
2467
2611
|
let pending = checkpoint?.pending;
|
|
2468
2612
|
const approvedCalls = checkpoint?.approvedCalls ?? [];
|
|
2613
|
+
const interruptHistory = checkpoint?.interruptHistory ?? [];
|
|
2469
2614
|
const save = () => {
|
|
2470
2615
|
ctx.currentNodeState.byKey[checkpointKey] = {
|
|
2471
2616
|
status: "tool_loop",
|
|
@@ -2473,6 +2618,7 @@ var runReasonToolLoop = async (args) => {
|
|
|
2473
2618
|
messages,
|
|
2474
2619
|
steps,
|
|
2475
2620
|
approvedCalls,
|
|
2621
|
+
interruptHistory,
|
|
2476
2622
|
finalizing,
|
|
2477
2623
|
...pending ? { pending } : {}
|
|
2478
2624
|
};
|
|
@@ -2551,6 +2697,10 @@ var runReasonToolLoop = async (args) => {
|
|
|
2551
2697
|
...step.warnings ? { warnings: step.warnings } : {}
|
|
2552
2698
|
});
|
|
2553
2699
|
if (toolCalls.length === 0) {
|
|
2700
|
+
if (interruptConfig?.mode === "required" && interruptHistory.length === 0)
|
|
2701
|
+
throw new Error(
|
|
2702
|
+
"useReason interrupts.mode is required, but the model returned a final answer without requesting human input."
|
|
2703
|
+
);
|
|
2554
2704
|
if (separateOutput && !finalizing) {
|
|
2555
2705
|
if (args.allowValidatedToolOutput && useReasonArgs.outputSchema) {
|
|
2556
2706
|
try {
|
|
@@ -2595,7 +2745,9 @@ var runReasonToolLoop = async (args) => {
|
|
|
2595
2745
|
"Provider requested a tool during schema-only finalization."
|
|
2596
2746
|
);
|
|
2597
2747
|
if (!reused) {
|
|
2598
|
-
allToolCalls.push(
|
|
2748
|
+
allToolCalls.push(
|
|
2749
|
+
...toolCalls.filter((call) => !interruptByToolName.has(call.name))
|
|
2750
|
+
);
|
|
2599
2751
|
messages.push({
|
|
2600
2752
|
role: "assistant",
|
|
2601
2753
|
content: step.text,
|
|
@@ -2605,6 +2757,84 @@ var runReasonToolLoop = async (args) => {
|
|
|
2605
2757
|
}
|
|
2606
2758
|
pending = step;
|
|
2607
2759
|
save();
|
|
2760
|
+
const interruptCalls = toolCalls.filter(
|
|
2761
|
+
(call) => interruptByToolName.has(call.name)
|
|
2762
|
+
);
|
|
2763
|
+
if (interruptCalls.length > 0) {
|
|
2764
|
+
if (toolCalls.length !== 1)
|
|
2765
|
+
throw new Error(
|
|
2766
|
+
"A useReason interrupt contract call must be the only tool call in its model turn."
|
|
2767
|
+
);
|
|
2768
|
+
const interruptCall = interruptCalls[0];
|
|
2769
|
+
if (!interruptCall)
|
|
2770
|
+
throw new Error("Missing useReason interrupt control call.");
|
|
2771
|
+
if (interruptHistory.length >= (interruptConfig?.maxRequests ?? 1))
|
|
2772
|
+
throw new Error(
|
|
2773
|
+
`useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
|
|
2774
|
+
);
|
|
2775
|
+
const resolved = interruptByToolName.get(interruptCall.name);
|
|
2776
|
+
if (!resolved)
|
|
2777
|
+
throw new Error(
|
|
2778
|
+
`Unknown useReason interrupt contract tool "${interruptCall.name}".`
|
|
2779
|
+
);
|
|
2780
|
+
if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
|
|
2781
|
+
pending = void 0;
|
|
2782
|
+
save();
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
const request = parseWithSchema(
|
|
2786
|
+
resolved.contract.requestSchema,
|
|
2787
|
+
interruptCall.input,
|
|
2788
|
+
`useReason interrupt contract "${resolved.name}" request`
|
|
2789
|
+
);
|
|
2790
|
+
traceSpan?.addEvent?.("useReason.interrupt.requested", {
|
|
2791
|
+
checkpointKey,
|
|
2792
|
+
contract: resolved.name,
|
|
2793
|
+
interruptIndex: interruptHistory.length
|
|
2794
|
+
});
|
|
2795
|
+
const question = request && typeof request === "object" && !Array.isArray(request) && typeof request.question === "string" ? String(request.question) : void 0;
|
|
2796
|
+
const response = await awaitInterruptInternal({
|
|
2797
|
+
request: {
|
|
2798
|
+
kind: "custom",
|
|
2799
|
+
request,
|
|
2800
|
+
contract: resolved.name,
|
|
2801
|
+
...question ? { question } : {},
|
|
2802
|
+
schemaId: resolved.contract.schemaId,
|
|
2803
|
+
schemaVersion: resolved.contract.schemaVersion
|
|
2804
|
+
},
|
|
2805
|
+
responseSchema: resolved.contract.responseSchema,
|
|
2806
|
+
id: `${id ?? "reason"}:${resolved.name}:${interruptHistory.length + 1}`,
|
|
2807
|
+
meta: {
|
|
2808
|
+
__kortyxReason: {
|
|
2809
|
+
opId,
|
|
2810
|
+
stepIndex,
|
|
2811
|
+
interruptIndex: interruptHistory.length,
|
|
2812
|
+
contract: resolved.name
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
const result2 = normalizeToolResult(interruptCall, response);
|
|
2817
|
+
toolResults.push(result2);
|
|
2818
|
+
messages.push({
|
|
2819
|
+
role: "tool",
|
|
2820
|
+
content: result2.content,
|
|
2821
|
+
toolCallId: result2.toolCallId,
|
|
2822
|
+
name: result2.name
|
|
2823
|
+
});
|
|
2824
|
+
interruptHistory.push({
|
|
2825
|
+
contract: resolved.name,
|
|
2826
|
+
request,
|
|
2827
|
+
response
|
|
2828
|
+
});
|
|
2829
|
+
traceSpan?.addEvent?.("useReason.interrupt.resolved", {
|
|
2830
|
+
checkpointKey,
|
|
2831
|
+
contract: resolved.name,
|
|
2832
|
+
interruptIndex: interruptHistory.length - 1
|
|
2833
|
+
});
|
|
2834
|
+
pending = void 0;
|
|
2835
|
+
save();
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2608
2838
|
for (const toolCall of toolCalls) {
|
|
2609
2839
|
if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
|
|
2610
2840
|
await replayToolObservations(
|
|
@@ -2814,7 +3044,11 @@ var runReasonToolLoop = async (args) => {
|
|
|
2814
3044
|
...finalOutput !== void 0 ? { output: finalOutput } : {},
|
|
2815
3045
|
toolCalls: allToolCalls,
|
|
2816
3046
|
toolResults: allToolResults,
|
|
2817
|
-
steps
|
|
3047
|
+
steps,
|
|
3048
|
+
...interruptHistory.length > 0 ? { interruptHistory } : {},
|
|
3049
|
+
...isLegacyInterruptContracts(interruptConfig) && interruptHistory.length > 0 ? {
|
|
3050
|
+
interruptResponse: interruptHistory.at(-1)?.response
|
|
3051
|
+
} : {}
|
|
2818
3052
|
};
|
|
2819
3053
|
traceSpan?.end?.({
|
|
2820
3054
|
...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
|
|
@@ -2825,7 +3059,8 @@ var runReasonToolLoop = async (args) => {
|
|
|
2825
3059
|
textLength: finalText.length,
|
|
2826
3060
|
toolCallCount: allToolCalls.length,
|
|
2827
3061
|
toolResultCount: allToolResults.length,
|
|
2828
|
-
toolStepCount: steps.length
|
|
3062
|
+
toolStepCount: steps.length,
|
|
3063
|
+
interruptCount: interruptHistory.length
|
|
2829
3064
|
},
|
|
2830
3065
|
telemetry: {
|
|
2831
3066
|
...useReasonArgs.telemetry ?? {},
|
|
@@ -2855,7 +3090,7 @@ var resolveEffectiveReasoningIncludeThoughts = (args) => args.reasoning?.include
|
|
|
2855
3090
|
var resolveEffectiveResponseFormatType = (args) => args.responseFormat?.type ?? args.model.options?.responseFormat?.type;
|
|
2856
3091
|
var assertReasoningThoughtsCompatibility = (args) => {
|
|
2857
3092
|
if (!resolveEffectiveReasoningIncludeThoughts(args)) return;
|
|
2858
|
-
const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
|
|
3093
|
+
const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
|
|
2859
3094
|
if (!usesStructuredOutput) return;
|
|
2860
3095
|
throw new Error(
|
|
2861
3096
|
"useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
|
|
@@ -2863,6 +3098,10 @@ var assertReasoningThoughtsCompatibility = (args) => {
|
|
|
2863
3098
|
};
|
|
2864
3099
|
async function runReason(args) {
|
|
2865
3100
|
assertReasoningThoughtsCompatibility(args);
|
|
3101
|
+
const interruptConfig = normalizeReasonInterrupts({
|
|
3102
|
+
interrupt: args.interrupt,
|
|
3103
|
+
interrupts: args.interrupts
|
|
3104
|
+
});
|
|
2866
3105
|
const ctx = getHookContext();
|
|
2867
3106
|
const abortSignal = (0, import_core6.combineAbortSignals)(
|
|
2868
3107
|
ctx.node.abortSignal,
|
|
@@ -2877,20 +3116,23 @@ async function runReason(args) {
|
|
|
2877
3116
|
args.outputSchema,
|
|
2878
3117
|
args.responseFormat ?? args.model.options?.responseFormat
|
|
2879
3118
|
);
|
|
3119
|
+
const usesLegacyNoToolInterrupt = Boolean(
|
|
3120
|
+
args.interrupt && !args.interrupts && !args.tools?.length
|
|
3121
|
+
);
|
|
2880
3122
|
args = {
|
|
2881
3123
|
...args,
|
|
2882
3124
|
abortSignal,
|
|
2883
|
-
...!
|
|
3125
|
+
...!usesLegacyNoToolInterrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
|
|
2884
3126
|
};
|
|
2885
3127
|
const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
|
|
2886
|
-
const
|
|
3128
|
+
const provisionalOpId = createRuntimeId2();
|
|
2887
3129
|
const checkpointKey = resolveReasonCheckpointKey({
|
|
2888
3130
|
...id ? { id } : {},
|
|
2889
3131
|
autoIndex: ctx.reasonCallIndex++
|
|
2890
3132
|
});
|
|
2891
|
-
const
|
|
2892
|
-
|
|
2893
|
-
);
|
|
3133
|
+
const checkpointValue = ctx.currentNodeState.byKey[checkpointKey];
|
|
3134
|
+
const opId = checkpointValue && typeof checkpointValue === "object" && "status" in checkpointValue && checkpointValue.status === "tool_loop" && "opId" in checkpointValue && typeof checkpointValue.opId === "string" ? checkpointValue.opId : provisionalOpId;
|
|
3135
|
+
const existingCompleted = readReasonCompletedCheckpoint(checkpointValue);
|
|
2894
3136
|
if (existingCompleted) {
|
|
2895
3137
|
await replayToolObservations(
|
|
2896
3138
|
existingCompleted.result.steps?.flatMap(
|
|
@@ -2899,9 +3141,7 @@ async function runReason(args) {
|
|
|
2899
3141
|
);
|
|
2900
3142
|
return existingCompleted.result;
|
|
2901
3143
|
}
|
|
2902
|
-
const existingCheckpoint = readReasonCheckpoint(
|
|
2903
|
-
ctx.currentNodeState.byKey[checkpointKey]
|
|
2904
|
-
);
|
|
3144
|
+
const existingCheckpoint = readReasonCheckpoint(checkpointValue);
|
|
2905
3145
|
const reasonMeta = typeof id === "string" ? { id, opId } : { opId };
|
|
2906
3146
|
return withSafeTraceSpan(
|
|
2907
3147
|
getReasonTraceAdapter(),
|
|
@@ -2920,7 +3160,8 @@ async function runReason(args) {
|
|
|
2920
3160
|
...args.telemetry?.prompt?.version !== void 0 ? { promptVersion: args.telemetry.prompt.version } : {},
|
|
2921
3161
|
...args.telemetry?.prompt?.type ? { promptType: args.telemetry.prompt.type } : {},
|
|
2922
3162
|
hasOutputSchema: Boolean(args.outputSchema),
|
|
2923
|
-
hasInterrupt: Boolean(
|
|
3163
|
+
hasInterrupt: Boolean(interruptConfig),
|
|
3164
|
+
interruptContractCount: Object.keys(interruptConfig?.contracts ?? {}).length,
|
|
2924
3165
|
hasStructured: Boolean(args.structured),
|
|
2925
3166
|
hasTools: Boolean(args.tools?.length)
|
|
2926
3167
|
},
|
|
@@ -2930,7 +3171,7 @@ async function runReason(args) {
|
|
|
2930
3171
|
}
|
|
2931
3172
|
},
|
|
2932
3173
|
async (traceSpan) => {
|
|
2933
|
-
if (args.tools?.length) {
|
|
3174
|
+
if ((args.tools?.length || args.interrupts) && !existingCheckpoint) {
|
|
2934
3175
|
return runReasonToolLoop({
|
|
2935
3176
|
useReasonArgs: args,
|
|
2936
3177
|
allowValidatedToolOutput,
|
|
@@ -3199,6 +3440,24 @@ function useReason2(args) {
|
|
|
3199
3440
|
return useReason(args);
|
|
3200
3441
|
}
|
|
3201
3442
|
function useInterrupt(args) {
|
|
3443
|
+
if ("contract" in args) {
|
|
3444
|
+
const contract = defineInterruptContract(args.contract);
|
|
3445
|
+
return awaitInterruptInternal({
|
|
3446
|
+
request: {
|
|
3447
|
+
kind: "custom",
|
|
3448
|
+
request: parseWithSchema(
|
|
3449
|
+
contract.requestSchema,
|
|
3450
|
+
args.request,
|
|
3451
|
+
"useInterrupt contract request"
|
|
3452
|
+
),
|
|
3453
|
+
schemaId: contract.schemaId,
|
|
3454
|
+
schemaVersion: contract.schemaVersion,
|
|
3455
|
+
...args.id ? { id: args.id } : {},
|
|
3456
|
+
...args.meta ? { meta: args.meta } : {}
|
|
3457
|
+
},
|
|
3458
|
+
responseSchema: contract.responseSchema
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3202
3461
|
return awaitInterruptInternal(args);
|
|
3203
3462
|
}
|
|
3204
3463
|
function useAbortSignal() {
|
|
@@ -3697,6 +3956,7 @@ async function runWorkflowCall(args, task) {
|
|
|
3697
3956
|
WorkflowCallError,
|
|
3698
3957
|
completeResponse,
|
|
3699
3958
|
createWorkflowHooks,
|
|
3959
|
+
defineInterruptContract,
|
|
3700
3960
|
emitWorkflowCall,
|
|
3701
3961
|
parallel,
|
|
3702
3962
|
reportError,
|