@kortyx/agent 0.39.0 → 0.40.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 +24 -0
- package/README.md +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +195 -67
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,30 @@
|
|
|
8
8
|
event replay, explicit cancellation and optional PostgreSQL recovery.
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [0.40.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.39.0...agent-v0.40.0) (2026-10-18)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **studio:** drain evaluation workers and consumer endpoints safely ([#365](https://github.com/kortyx-io/kortyx/issues/365)) ([0bcd853](https://github.com/kortyx-io/kortyx/commit/0bcd85310f5ccf59772b2fa9ed4dd28d03394546))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* retain workflow catalogs and text interrupt questions ([#369](https://github.com/kortyx-io/kortyx/issues/369)) ([7608f65](https://github.com/kortyx-io/kortyx/commit/7608f6503167ed94feda30a1f63db50f475557d6))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Dependencies
|
|
25
|
+
|
|
26
|
+
* The following workspace dependencies were updated
|
|
27
|
+
* dependencies
|
|
28
|
+
* @kortyx/runtime bumped to 0.26.0
|
|
29
|
+
* @kortyx/prompts bumped to 0.2.3
|
|
30
|
+
* @kortyx/usage-telemetry bumped to 0.5.0
|
|
31
|
+
* @kortyx/telemetry-contracts bumped to 0.19.0
|
|
32
|
+
* devDependencies
|
|
33
|
+
* @kortyx/hooks bumped to 0.35.1
|
|
34
|
+
|
|
11
35
|
## [0.39.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.38.1...agent-v0.39.0) (2026-10-16)
|
|
12
36
|
|
|
13
37
|
|
package/README.md
CHANGED
|
@@ -9,6 +9,13 @@ Agent creation, chat route handlers, HTTP helpers, and stream orchestration for
|
|
|
9
9
|
|
|
10
10
|
Most application code should import these APIs from `kortyx`. Use `@kortyx/agent` directly when you are building framework adapters or want the lower-level agent package without the full facade.
|
|
11
11
|
|
|
12
|
+
Topology identity excludes handoffs inferred from JavaScript function source, so
|
|
13
|
+
source publication and bundled execution share a revision even when bundling hides
|
|
14
|
+
a target. Inferred handoffs and source-discovered child links remain catalog
|
|
15
|
+
metadata. After upgrading the SDK and CLI, rebuild the application and republish
|
|
16
|
+
its source catalog. Previously inferred handoffs can receive a new topology hash
|
|
17
|
+
once; historical revisions and run references remain readable without migration.
|
|
18
|
+
|
|
12
19
|
## Install
|
|
13
20
|
|
|
14
21
|
```bash
|
|
@@ -112,6 +119,16 @@ The default memory store only survives within its process. See
|
|
|
112
119
|
[execution resilience](../../docs/evals/execution-resilience.md) for configuration,
|
|
113
120
|
retention, legacy compatibility and upgrade order.
|
|
114
121
|
|
|
122
|
+
For planned retirement, `await handleEvals.drain()` stops new admissions and
|
|
123
|
+
finishes accepted local/queued executions without aborting them. Keep HTTP and
|
|
124
|
+
the store open for observation/cancellation; `close()` remains explicit abort.
|
|
125
|
+
With one consumer, drain Studio workers first while the consumer still accepts
|
|
126
|
+
later cases, then drain the consumer after Studio persists results. Overlapping
|
|
127
|
+
consumers require a compatible replacement and shared durable execution storage.
|
|
128
|
+
The built-in stores support this; custom `EvalExecutionStore` implementations need
|
|
129
|
+
`queued` and `claimQueued` for drain. Upgrade agent and runtime together. See the
|
|
130
|
+
linked guide for ordering, failure handling and resource lifetime.
|
|
131
|
+
|
|
115
132
|
### Evaluation settings and errors
|
|
116
133
|
|
|
117
134
|
`createEvals` supports `defaults: { repetitions, concurrency, caseTimeoutMs }`,
|
package/dist/index.d.ts
CHANGED
|
@@ -294,6 +294,9 @@ declare function createEvalRouteHandler({ evals, serviceKey, maxCases, maxActive
|
|
|
294
294
|
executionStore?: EvalExecutionStore;
|
|
295
295
|
onExecution?: (completion: Promise<void>) => void;
|
|
296
296
|
}): ((request: Request) => Promise<Response>) & {
|
|
297
|
+
/** Stop admissions and finish accepted jobs; keep HTTP/store alive for observers. */
|
|
298
|
+
drain: () => Promise<void>;
|
|
299
|
+
/** Explicitly abort local execution, then wait for cleanup. */
|
|
297
300
|
close: () => Promise<void>;
|
|
298
301
|
};
|
|
299
302
|
|
package/dist/index.js
CHANGED
|
@@ -165,7 +165,7 @@ var require_internal = __commonJS({
|
|
|
165
165
|
var import_node2 = require("@kortyx/usage-telemetry/node");
|
|
166
166
|
var package_default2 = {
|
|
167
167
|
name: "@kortyx/hooks",
|
|
168
|
-
version: "0.35.
|
|
168
|
+
version: "0.35.1",
|
|
169
169
|
private: false,
|
|
170
170
|
description: "Lifecycle hooks and extension points for Kortyx.",
|
|
171
171
|
keywords: [
|
|
@@ -801,7 +801,7 @@ var require_dist = __commonJS({
|
|
|
801
801
|
var import_node2 = require("@kortyx/usage-telemetry/node");
|
|
802
802
|
var package_default2 = {
|
|
803
803
|
name: "@kortyx/hooks",
|
|
804
|
-
version: "0.35.
|
|
804
|
+
version: "0.35.1",
|
|
805
805
|
private: false,
|
|
806
806
|
description: "Lifecycle hooks and extension points for Kortyx.",
|
|
807
807
|
keywords: [
|
|
@@ -5768,7 +5768,10 @@ var projectWorkflowTopology = (args) => {
|
|
|
5768
5768
|
workflowId: args.workflow.id,
|
|
5769
5769
|
nodes: nodes.map((node) => ({ id: node.id, ...node._execution })),
|
|
5770
5770
|
edges,
|
|
5771
|
-
|
|
5771
|
+
// Handoffs inferred from function source are catalog enrichment. Bundlers
|
|
5772
|
+
// can hide their targets without changing the executable graph. Keep the
|
|
5773
|
+
// legacy empty list so graphs without inferred handoffs retain their hash.
|
|
5774
|
+
transitions: []
|
|
5772
5775
|
});
|
|
5773
5776
|
const topologyHash = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(hashInput)).digest("hex");
|
|
5774
5777
|
return {
|
|
@@ -5847,7 +5850,7 @@ var import_node = require("@kortyx/usage-telemetry/node");
|
|
|
5847
5850
|
// package.json
|
|
5848
5851
|
var package_default = {
|
|
5849
5852
|
name: "@kortyx/agent",
|
|
5850
|
-
version: "0.
|
|
5853
|
+
version: "0.40.0",
|
|
5851
5854
|
private: false,
|
|
5852
5855
|
description: "Agent composition utilities for Kortyx.",
|
|
5853
5856
|
keywords: [
|
|
@@ -10875,8 +10878,32 @@ function createEvalExecutionTransport({
|
|
|
10875
10878
|
}) {
|
|
10876
10879
|
const owner = (0, import_node_crypto6.randomUUID)();
|
|
10877
10880
|
const active = /* @__PURE__ */ new Map();
|
|
10878
|
-
let ticking
|
|
10881
|
+
let ticking;
|
|
10882
|
+
let draining = false;
|
|
10879
10883
|
let stopped = false;
|
|
10884
|
+
let drain;
|
|
10885
|
+
let closing;
|
|
10886
|
+
let retirementFailure;
|
|
10887
|
+
const unsupportedRetirement = new Error(
|
|
10888
|
+
"Execution store requires queued and claimQueued for graceful retirement."
|
|
10889
|
+
);
|
|
10890
|
+
const admissions = /* @__PURE__ */ new Set();
|
|
10891
|
+
const heartbeats = /* @__PURE__ */ new Set();
|
|
10892
|
+
const failedRetirement = () => {
|
|
10893
|
+
retirementFailure ??= new Error(
|
|
10894
|
+
"Consumer evaluation retirement could not be confirmed."
|
|
10895
|
+
);
|
|
10896
|
+
};
|
|
10897
|
+
const finish = async (id3, state) => {
|
|
10898
|
+
try {
|
|
10899
|
+
await store.finish(id3, owner, state);
|
|
10900
|
+
if ((await store.status(id3))?.state !== state)
|
|
10901
|
+
throw new Error("Consumer completion was not confirmed.");
|
|
10902
|
+
} catch (error) {
|
|
10903
|
+
failedRetirement();
|
|
10904
|
+
throw error;
|
|
10905
|
+
}
|
|
10906
|
+
};
|
|
10880
10907
|
const key = (id3) => (0, import_node_crypto6.createHash)("sha256").update(`${scope}\0${id3}`).digest("hex");
|
|
10881
10908
|
const fail = async (job, error) => {
|
|
10882
10909
|
const issue2 = createEvalIssue("reporting", error);
|
|
@@ -10893,7 +10920,11 @@ function createEvalExecutionTransport({
|
|
|
10893
10920
|
const timer2 = setInterval(() => {
|
|
10894
10921
|
if (renewing) return;
|
|
10895
10922
|
renewing = true;
|
|
10896
|
-
|
|
10923
|
+
const pending = Promise.resolve().then(
|
|
10924
|
+
() => store.heartbeat(job.id, owner)
|
|
10925
|
+
);
|
|
10926
|
+
heartbeats.add(pending);
|
|
10927
|
+
void pending.then(
|
|
10897
10928
|
(current) => {
|
|
10898
10929
|
if (!current) controller.abort("lease-lost");
|
|
10899
10930
|
else if (current.cancelRequested)
|
|
@@ -10901,6 +10932,7 @@ function createEvalExecutionTransport({
|
|
|
10901
10932
|
},
|
|
10902
10933
|
() => controller.abort("lease-lost")
|
|
10903
10934
|
).finally(() => {
|
|
10935
|
+
heartbeats.delete(pending);
|
|
10904
10936
|
renewing = false;
|
|
10905
10937
|
});
|
|
10906
10938
|
}, store.heartbeatIntervalMs);
|
|
@@ -10935,9 +10967,8 @@ function createEvalExecutionTransport({
|
|
|
10935
10967
|
onProgress: (event) => store.append(job.id, owner, { type: "progress", event })
|
|
10936
10968
|
});
|
|
10937
10969
|
await store.append(job.id, owner, { type: "result", result });
|
|
10938
|
-
await
|
|
10970
|
+
await finish(
|
|
10939
10971
|
job.id,
|
|
10940
|
-
owner,
|
|
10941
10972
|
result.status === "cancelled" ? "cancelled" : "completed"
|
|
10942
10973
|
);
|
|
10943
10974
|
} catch (error) {
|
|
@@ -10954,12 +10985,12 @@ function createEvalExecutionTransport({
|
|
|
10954
10985
|
}
|
|
10955
10986
|
);
|
|
10956
10987
|
await fail(job, error);
|
|
10957
|
-
await
|
|
10988
|
+
await finish(
|
|
10958
10989
|
job.id,
|
|
10959
|
-
owner,
|
|
10960
10990
|
controller.signal.reason === "user-cancelled" ? "cancelled" : "completed"
|
|
10961
10991
|
);
|
|
10962
10992
|
} catch {
|
|
10993
|
+
failedRetirement();
|
|
10963
10994
|
}
|
|
10964
10995
|
} finally {
|
|
10965
10996
|
clearInterval(timer2);
|
|
@@ -10967,25 +10998,30 @@ function createEvalExecutionTransport({
|
|
|
10967
10998
|
active.delete(job.id);
|
|
10968
10999
|
}
|
|
10969
11000
|
};
|
|
10970
|
-
const tick =
|
|
10971
|
-
if (
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
11001
|
+
const tick = (selected) => {
|
|
11002
|
+
if (ticking) return Promise.resolve();
|
|
11003
|
+
if (stopped || draining && !selected) return Promise.resolve();
|
|
11004
|
+
ticking = Promise.resolve().then(async () => {
|
|
11005
|
+
try {
|
|
11006
|
+
while (!stopped && (!draining || selected)) {
|
|
11007
|
+
const job = selected ? await store.claimQueued(owner, scope, limits, selected) : await store.claim(owner, scope, limits);
|
|
11008
|
+
if (!job) break;
|
|
11009
|
+
if (stopped) break;
|
|
11010
|
+
const controller = new AbortController();
|
|
11011
|
+
const completion = run(job, controller);
|
|
11012
|
+
active.set(job.id, { controller, completion });
|
|
11013
|
+
try {
|
|
11014
|
+
onExecution?.(completion);
|
|
11015
|
+
} catch {
|
|
11016
|
+
}
|
|
10983
11017
|
}
|
|
11018
|
+
} catch {
|
|
11019
|
+
failedRetirement();
|
|
11020
|
+
} finally {
|
|
11021
|
+
ticking = void 0;
|
|
10984
11022
|
}
|
|
10985
|
-
}
|
|
10986
|
-
|
|
10987
|
-
ticking = false;
|
|
10988
|
-
}
|
|
11023
|
+
});
|
|
11024
|
+
return ticking;
|
|
10989
11025
|
};
|
|
10990
11026
|
const timer = setInterval(() => {
|
|
10991
11027
|
void tick();
|
|
@@ -11148,20 +11184,21 @@ function createEvalExecutionTransport({
|
|
|
11148
11184
|
},
|
|
11149
11185
|
async submit(request, body) {
|
|
11150
11186
|
const negotiated = request.headers.get("x-kortyx-eval-protocol") === protocol;
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11187
|
+
const retiring = draining;
|
|
11188
|
+
const refused = () => problem(
|
|
11189
|
+
new import_errors13.KortyxError(
|
|
11190
|
+
"EVAL_EXECUTOR_BUSY",
|
|
11191
|
+
"Eval executor is draining; execution was not accepted.",
|
|
11192
|
+
{
|
|
11193
|
+
category: "request",
|
|
11194
|
+
status: 503,
|
|
11195
|
+
retryable: true,
|
|
11196
|
+
safeMessage: "Eval executor is draining; execution was not accepted."
|
|
11197
|
+
}
|
|
11198
|
+
),
|
|
11199
|
+
negotiated
|
|
11200
|
+
);
|
|
11201
|
+
if (stopped || retiring && !negotiated) return refused();
|
|
11165
11202
|
const id3 = negotiated ? request.headers.get("idempotency-key") : (0, import_node_crypto6.randomUUID)();
|
|
11166
11203
|
if (!id3 || !uuid.test(id3))
|
|
11167
11204
|
return Response.json(
|
|
@@ -11176,31 +11213,75 @@ function createEvalExecutionTransport({
|
|
|
11176
11213
|
},
|
|
11177
11214
|
{ status: 410 }
|
|
11178
11215
|
);
|
|
11216
|
+
const fingerprint = (0, import_node_crypto6.createHash)("sha256").update(canonical2(body)).digest("hex");
|
|
11179
11217
|
try {
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11218
|
+
let admitted;
|
|
11219
|
+
const pending = new Promise((resolve2) => {
|
|
11220
|
+
admitted = resolve2;
|
|
11221
|
+
});
|
|
11222
|
+
admissions.add(pending);
|
|
11223
|
+
try {
|
|
11224
|
+
if (negotiated && request.headers.get("x-kortyx-eval-generation") !== await store.generation())
|
|
11225
|
+
return Response.json(
|
|
11226
|
+
{
|
|
11227
|
+
error: "Execution storage changed; execution status is unknown.",
|
|
11228
|
+
code: "EVAL_EXECUTION_STORE_CHANGED"
|
|
11229
|
+
},
|
|
11230
|
+
{ status: 409 }
|
|
11231
|
+
);
|
|
11232
|
+
if (retiring) {
|
|
11233
|
+
const existing = await store.get(key(id3));
|
|
11234
|
+
if (!existing) return refused();
|
|
11235
|
+
if (existing.fingerprint !== fingerprint)
|
|
11236
|
+
throw new import_errors13.KortyxError(
|
|
11237
|
+
"EVAL_EXECUTION_CONFLICT",
|
|
11238
|
+
"Execution identity was reused with a different request.",
|
|
11239
|
+
{
|
|
11240
|
+
category: "request",
|
|
11241
|
+
status: 409,
|
|
11242
|
+
retryable: false,
|
|
11243
|
+
safeMessage: "Execution identity was reused with a different request."
|
|
11244
|
+
}
|
|
11245
|
+
);
|
|
11246
|
+
return await observe(request, id3);
|
|
11247
|
+
}
|
|
11248
|
+
try {
|
|
11249
|
+
await store.submit(
|
|
11250
|
+
{
|
|
11251
|
+
id: key(id3),
|
|
11252
|
+
scope,
|
|
11253
|
+
fingerprint,
|
|
11254
|
+
group: key(body.attempt?.evaluationId ?? id3),
|
|
11255
|
+
concurrency: body.concurrency,
|
|
11256
|
+
weight: body.attempt ? 1 : body.concurrency,
|
|
11257
|
+
payload: body,
|
|
11258
|
+
queued: negotiated
|
|
11259
|
+
},
|
|
11260
|
+
limits
|
|
11261
|
+
);
|
|
11262
|
+
} catch (error) {
|
|
11263
|
+
if (!(error instanceof import_errors13.KortyxError) || ![
|
|
11264
|
+
"EVAL_EXECUTOR_BUSY",
|
|
11265
|
+
"EVAL_EXECUTION_CONFLICT",
|
|
11266
|
+
"EVAL_CONCURRENCY_UNSUPPORTED",
|
|
11267
|
+
"EVAL_CONCURRENCY_CHANGED",
|
|
11268
|
+
"EVAL_EVIDENCE_LIMIT"
|
|
11269
|
+
].includes(error.code))
|
|
11270
|
+
failedRetirement();
|
|
11271
|
+
throw error;
|
|
11272
|
+
}
|
|
11273
|
+
} finally {
|
|
11274
|
+
admissions.delete(pending);
|
|
11275
|
+
admitted();
|
|
11276
|
+
}
|
|
11277
|
+
if (stopped && !negotiated) {
|
|
11278
|
+
throw new Error("Consumer execution was explicitly closed.");
|
|
11279
|
+
}
|
|
11201
11280
|
await tick();
|
|
11202
11281
|
if (!negotiated) {
|
|
11203
11282
|
while ((await store.status(key(id3)))?.state === "queued") {
|
|
11283
|
+
if (stopped)
|
|
11284
|
+
throw new Error("Consumer execution was explicitly closed.");
|
|
11204
11285
|
request.signal.throwIfAborted();
|
|
11205
11286
|
await wait(10, request.signal);
|
|
11206
11287
|
await tick();
|
|
@@ -11208,7 +11289,7 @@ function createEvalExecutionTransport({
|
|
|
11208
11289
|
}
|
|
11209
11290
|
return await observe(request, id3, 0, negotiated);
|
|
11210
11291
|
} catch (error) {
|
|
11211
|
-
if (!negotiated && request.signal.aborted) {
|
|
11292
|
+
if (!negotiated && (request.signal.aborted || stopped)) {
|
|
11212
11293
|
active.get(key(id3))?.controller.abort("user-cancelled");
|
|
11213
11294
|
await store.cancel(key(id3)).catch(() => {
|
|
11214
11295
|
});
|
|
@@ -11222,11 +11303,53 @@ function createEvalExecutionTransport({
|
|
|
11222
11303
|
"x-kortyx-eval-generation": await store.generation()
|
|
11223
11304
|
};
|
|
11224
11305
|
},
|
|
11225
|
-
|
|
11306
|
+
drain() {
|
|
11307
|
+
draining = true;
|
|
11308
|
+
clearInterval(timer);
|
|
11309
|
+
drain ??= (async () => {
|
|
11310
|
+
try {
|
|
11311
|
+
await Promise.all(admissions);
|
|
11312
|
+
await ticking;
|
|
11313
|
+
if (!store.queued || !store.claimQueued) throw unsupportedRetirement;
|
|
11314
|
+
const selected = new Set(await store.queued(scope));
|
|
11315
|
+
if (selected.size > limits.maxQueuedExecutions)
|
|
11316
|
+
throw new Error("Retirement queue exceeds the configured bound.");
|
|
11317
|
+
while (!stopped) {
|
|
11318
|
+
if (selected.size) await tick([...selected]);
|
|
11319
|
+
for (const id3 of selected) {
|
|
11320
|
+
const status2 = await store.status(id3);
|
|
11321
|
+
if (!status2 || status2.state === "unknown") failedRetirement();
|
|
11322
|
+
if (status2?.state !== "queued") selected.delete(id3);
|
|
11323
|
+
}
|
|
11324
|
+
await Promise.all(
|
|
11325
|
+
[...active.values()].map((job) => job.completion)
|
|
11326
|
+
);
|
|
11327
|
+
if (!selected.size || retirementFailure) break;
|
|
11328
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
11329
|
+
}
|
|
11330
|
+
await Promise.all([...active.values()].map((job) => job.completion));
|
|
11331
|
+
await Promise.allSettled(heartbeats);
|
|
11332
|
+
if (stopped)
|
|
11333
|
+
throw new Error("Consumer retirement was explicitly aborted.");
|
|
11334
|
+
if (retirementFailure) throw retirementFailure;
|
|
11335
|
+
} catch (error) {
|
|
11336
|
+
failedRetirement();
|
|
11337
|
+
throw error === unsupportedRetirement ? unsupportedRetirement : retirementFailure;
|
|
11338
|
+
}
|
|
11339
|
+
})();
|
|
11340
|
+
return drain;
|
|
11341
|
+
},
|
|
11342
|
+
close() {
|
|
11226
11343
|
stopped = true;
|
|
11227
11344
|
clearInterval(timer);
|
|
11228
|
-
|
|
11229
|
-
|
|
11345
|
+
closing ??= (async () => {
|
|
11346
|
+
for (const job of active.values()) job.controller.abort("host-stopped");
|
|
11347
|
+
await Promise.all(admissions);
|
|
11348
|
+
await ticking;
|
|
11349
|
+
await Promise.all([...active.values()].map((job) => job.completion));
|
|
11350
|
+
await Promise.allSettled(heartbeats);
|
|
11351
|
+
})();
|
|
11352
|
+
return closing;
|
|
11230
11353
|
}
|
|
11231
11354
|
};
|
|
11232
11355
|
}
|
|
@@ -11360,7 +11483,12 @@ function createEvalRouteHandler({
|
|
|
11360
11483
|
return Response.json({ error: "Request cancelled." }, { status: 409 });
|
|
11361
11484
|
return execution.submit(request, body);
|
|
11362
11485
|
};
|
|
11363
|
-
return Object.assign(handler3, {
|
|
11486
|
+
return Object.assign(handler3, {
|
|
11487
|
+
/** Stop admissions and finish accepted jobs; keep HTTP/store alive for observers. */
|
|
11488
|
+
drain: execution.drain,
|
|
11489
|
+
/** Explicitly abort local execution, then wait for cleanup. */
|
|
11490
|
+
close: execution.close
|
|
11491
|
+
});
|
|
11364
11492
|
}
|
|
11365
11493
|
|
|
11366
11494
|
// src/evals/studio-judge.ts
|