runline 0.27.0 → 0.27.2
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/core/engine.d.ts +7 -0
- package/dist/core/engine.js +126 -8
- package/package.json +1 -1
package/dist/core/engine.d.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface EngineHooks {
|
|
|
46
46
|
* ignores resourceLimits, so the watchdog is the only memory backstop.
|
|
47
47
|
*/
|
|
48
48
|
export declare function shouldArmRssWatchdog(versions?: Partial<Record<string, string>>): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Ceiling on an armed timeout: setTimeout stores its delay in an int32,
|
|
51
|
+
* so past ~24.8 days it overflows and fires at 0ms. Oversized values are
|
|
52
|
+
* rejected with an error that teaches the caller to pass 0 — the explicit
|
|
53
|
+
* "no timeout" — instead of overflowing into an instant kill.
|
|
54
|
+
*/
|
|
55
|
+
export declare const MAX_TIMEOUT_MS: number;
|
|
49
56
|
/**
|
|
50
57
|
* Executes agent code in a node:worker_threads worker.
|
|
51
58
|
*
|
package/dist/core/engine.js
CHANGED
|
@@ -48,6 +48,13 @@ function workerShape(pluginNames, helpData, memoryLimitMb) {
|
|
|
48
48
|
export function shouldArmRssWatchdog(versions = process.versions) {
|
|
49
49
|
return Boolean(versions.bun);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Ceiling on an armed timeout: setTimeout stores its delay in an int32,
|
|
53
|
+
* so past ~24.8 days it overflows and fires at 0ms. Oversized values are
|
|
54
|
+
* rejected with an error that teaches the caller to pass 0 — the explicit
|
|
55
|
+
* "no timeout" — instead of overflowing into an instant kill.
|
|
56
|
+
*/
|
|
57
|
+
export const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
51
58
|
// Extra slack on top of the configured memory limit for the RSS watchdog,
|
|
52
59
|
// since whole-process RSS includes the host's own working set.
|
|
53
60
|
const RSS_WATCHDOG_SLACK_BYTES = 128 * 1024 * 1024;
|
|
@@ -149,6 +156,16 @@ export class ExecutionEngine {
|
|
|
149
156
|
const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs;
|
|
150
157
|
const memoryLimitBytes = options?.memoryLimitBytes ?? this.config.memoryLimitBytes;
|
|
151
158
|
const logs = [];
|
|
159
|
+
if (!Number.isFinite(timeoutMs) ||
|
|
160
|
+
timeoutMs < 0 ||
|
|
161
|
+
timeoutMs > MAX_TIMEOUT_MS) {
|
|
162
|
+
return {
|
|
163
|
+
result: null,
|
|
164
|
+
error: `Invalid timeoutMs: ${timeoutMs}. ` +
|
|
165
|
+
`Pass 0 (no timeout) or 1..${MAX_TIMEOUT_MS} ms.`,
|
|
166
|
+
logs,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
152
169
|
const plugins = this.registry.listPlugins();
|
|
153
170
|
const pluginNames = plugins.map((p) => p.name);
|
|
154
171
|
const helpData = buildHelpData(plugins);
|
|
@@ -190,21 +207,28 @@ export class ExecutionEngine {
|
|
|
190
207
|
settled = true;
|
|
191
208
|
clearTimeout(timeoutTimer);
|
|
192
209
|
clearInterval(rssTimer);
|
|
210
|
+
worker.unref?.();
|
|
193
211
|
worker.off("message", onMessage);
|
|
194
212
|
worker.off("error", onError);
|
|
195
213
|
worker.off("exit", onExit);
|
|
196
214
|
this.release(entry, pooled, retire);
|
|
197
215
|
resolve(r);
|
|
198
216
|
};
|
|
217
|
+
// The acquire-time unref must not let the host exit mid-run: hold
|
|
218
|
+
// the loop for the run's duration, whether or not a timer is armed.
|
|
219
|
+
worker.ref?.();
|
|
199
220
|
// A spinning body blocks the worker's event loop, so it can never be
|
|
200
221
|
// asked to stop — terminate is the only remedy and the worker is gone.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
// 0 means no timeout: the caller explicitly opted out of the backstop.
|
|
223
|
+
const timeoutTimer = timeoutMs === 0
|
|
224
|
+
? undefined
|
|
225
|
+
: setTimeout(() => {
|
|
226
|
+
finish({
|
|
227
|
+
result: null,
|
|
228
|
+
error: `Execution timed out after ${timeoutMs}ms`,
|
|
229
|
+
logs,
|
|
230
|
+
}, true);
|
|
231
|
+
}, timeoutMs);
|
|
208
232
|
let rssTimer;
|
|
209
233
|
if (shouldArmRssWatchdog()) {
|
|
210
234
|
const baselineRss = process.memoryUsage().rss;
|
|
@@ -435,6 +459,12 @@ function formatError(cause) {
|
|
|
435
459
|
// code gets a cwd-relative `require`, so a bare specifier would resolve
|
|
436
460
|
// against the caller's cwd instead of runline's own node_modules.
|
|
437
461
|
const ferrosearchPath = createRequire(import.meta.url).resolve("@shift-labs/ferrosearch");
|
|
462
|
+
// typebox ships ESM only; require() of an .mjs works everywhere runline
|
|
463
|
+
// runs (bun, and node since 22.12's require(esm)). Resolved host-side
|
|
464
|
+
// for the same reason as ferrosearch: eval'd worker code gets a
|
|
465
|
+
// cwd-relative require, so a bare specifier would resolve against the
|
|
466
|
+
// caller's cwd instead of runline's own node_modules.
|
|
467
|
+
const typeboxValuePath = createRequire(import.meta.url).resolve("typebox/value");
|
|
438
468
|
function buildHelpData(plugins) {
|
|
439
469
|
const data = {};
|
|
440
470
|
for (const p of plugins) {
|
|
@@ -443,6 +473,7 @@ function buildHelpData(plugins) {
|
|
|
443
473
|
access: a.access,
|
|
444
474
|
description: a.description,
|
|
445
475
|
inputs: helpInputs(a.inputSchema),
|
|
476
|
+
...(isTypedInputSchema(a.inputSchema) ? { schema: a.inputSchema } : {}),
|
|
446
477
|
}));
|
|
447
478
|
}
|
|
448
479
|
return data;
|
|
@@ -570,6 +601,9 @@ const __toJson = (v) => v === undefined ? undefined : JSON.parse(JSON.stringify(
|
|
|
570
601
|
// also means an index built by agent code is invisible to that limit.
|
|
571
602
|
const { FerroSearch } = require(${JSON.stringify(ferrosearchPath)});
|
|
572
603
|
|
|
604
|
+
// The host's own validator, for actions.check on typed schemas.
|
|
605
|
+
const { Check: __schemaCheck, Errors: __schemaErrors } = require(${JSON.stringify(typeboxValuePath)});
|
|
606
|
+
|
|
573
607
|
const __help = ${JSON.stringify(helpData)};
|
|
574
608
|
|
|
575
609
|
const __makeProxy = (path = []) => new Proxy(() => undefined, {
|
|
@@ -629,6 +663,82 @@ const __search = (() => {
|
|
|
629
663
|
return index;
|
|
630
664
|
})();
|
|
631
665
|
|
|
666
|
+
// instancePath "/members/0/externalId" → ["members", "0", "externalId"],
|
|
667
|
+
// with JSON-pointer escapes (~1 for /, ~0 for ~) undone per part.
|
|
668
|
+
const __pointerParts = (instancePath) =>
|
|
669
|
+
String(instancePath || '').split('/').filter((p) => p.length)
|
|
670
|
+
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
671
|
+
|
|
672
|
+
// ["members", "0", "externalId"] → "members[0].externalId"
|
|
673
|
+
const __fieldPath = (instancePath, prop) => {
|
|
674
|
+
const parts = __pointerParts(instancePath);
|
|
675
|
+
if (prop !== undefined) parts.push(String(prop));
|
|
676
|
+
let out = '';
|
|
677
|
+
for (const part of parts) {
|
|
678
|
+
if (/^\\d+$/.test(part)) out += '[' + part + ']';
|
|
679
|
+
else out += out ? '.' + part : part;
|
|
680
|
+
}
|
|
681
|
+
return out;
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const __valueAt = (root, instancePath) => {
|
|
685
|
+
let v = root;
|
|
686
|
+
for (const part of __pointerParts(instancePath)) {
|
|
687
|
+
if (v == null) return undefined;
|
|
688
|
+
v = v[part];
|
|
689
|
+
}
|
|
690
|
+
return v;
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const __valueType = (v) => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v;
|
|
694
|
+
|
|
695
|
+
// Deep validation for typed schemas (SHFT-1187): the same Check/Errors
|
|
696
|
+
// the host runs before execute, on the same schema, so the preflight
|
|
697
|
+
// verdict and the execution verdict cannot diverge. The familiar
|
|
698
|
+
// missing/unknown/typeErrors buckets are derived from the validator's
|
|
699
|
+
// structured errors — keyword and params, never message parsing — and
|
|
700
|
+
// the errors array carries each raw "<path> <message>" line exactly as
|
|
701
|
+
// a rejected execution would report it. Grouping type errors by instance
|
|
702
|
+
// path keeps a failed union one typeError instead of one per branch.
|
|
703
|
+
const __checkTyped = (entry, provided, signature) => {
|
|
704
|
+
if (__schemaCheck(entry.schema, provided)) {
|
|
705
|
+
return { ok: true, missing: [], unknown: [], typeErrors: [], errors: [], signature };
|
|
706
|
+
}
|
|
707
|
+
const missing = [];
|
|
708
|
+
const unknown = [];
|
|
709
|
+
const errors = [];
|
|
710
|
+
const byPath = new Map();
|
|
711
|
+
for (const err of __schemaErrors(entry.schema, provided)) {
|
|
712
|
+
errors.push((err.instancePath || '/') + ' ' + err.message);
|
|
713
|
+
const params = err.params || {};
|
|
714
|
+
if (err.keyword === 'required') {
|
|
715
|
+
for (const prop of params.requiredProperties || []) missing.push(__fieldPath(err.instancePath, prop));
|
|
716
|
+
} else if (err.keyword === 'additionalProperties') {
|
|
717
|
+
for (const prop of params.additionalProperties || []) unknown.push(__fieldPath(err.instancePath, prop));
|
|
718
|
+
} else {
|
|
719
|
+
const group = byPath.get(err.instancePath) || [];
|
|
720
|
+
group.push(err);
|
|
721
|
+
byPath.set(err.instancePath, group);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const typeErrors = [];
|
|
725
|
+
for (const [instancePath, group] of byPath) {
|
|
726
|
+
const field = __fieldPath(instancePath) || '(input)';
|
|
727
|
+
const value = __valueAt(provided, instancePath);
|
|
728
|
+
const consts = group.filter((e) => e.keyword === 'const' && 'allowedValue' in (e.params || {}));
|
|
729
|
+
const typeErr = group.find((e) => e.keyword === 'type' && (e.params || {}).type);
|
|
730
|
+
if (consts.length) {
|
|
731
|
+
// A union of literals: the branches ARE the allowed values.
|
|
732
|
+
typeErrors.push({ field, expected: consts.map((e) => String(e.params.allowedValue)).join(' | '), actual: __fmt(value) });
|
|
733
|
+
} else if (typeErr) {
|
|
734
|
+
typeErrors.push({ field, expected: String(typeErr.params.type), actual: __valueType(value) });
|
|
735
|
+
} else {
|
|
736
|
+
typeErrors.push({ field, expected: group[0].message, actual: __fmt(value) });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return { ok: false, missing, unknown, typeErrors, errors, signature };
|
|
740
|
+
};
|
|
741
|
+
|
|
632
742
|
const __actionsApi = {
|
|
633
743
|
list(plugin) {
|
|
634
744
|
const paths = Object.keys(__index);
|
|
@@ -667,6 +777,13 @@ const __actionsApi = {
|
|
|
667
777
|
const near = __actionsApi.find(path, 3).map((n) => n.path);
|
|
668
778
|
return { ok: false, error: 'Unknown action: ' + path, suggestions: near };
|
|
669
779
|
}
|
|
780
|
+
const signature = __formatSignature(hit.plugin, hit.entry);
|
|
781
|
+
if (hit.entry.schema) {
|
|
782
|
+
// Mirror the host's normalizeActionInput: a check with no args is
|
|
783
|
+
// a call with an empty argument set; anything else — null, a
|
|
784
|
+
// scalar — is validated as given, exactly as execute would.
|
|
785
|
+
return __checkTyped(hit.entry, args === undefined ? {} : args, signature);
|
|
786
|
+
}
|
|
670
787
|
const inputs = hit.entry.inputs || {};
|
|
671
788
|
const provided = args && typeof args === 'object' ? args : {};
|
|
672
789
|
const missing = [];
|
|
@@ -695,7 +812,8 @@ const __actionsApi = {
|
|
|
695
812
|
missing,
|
|
696
813
|
unknown,
|
|
697
814
|
typeErrors,
|
|
698
|
-
|
|
815
|
+
errors: [],
|
|
816
|
+
signature,
|
|
699
817
|
};
|
|
700
818
|
},
|
|
701
819
|
};
|