@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
171 lines • 7.1 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { concat } from "../../../utils/bytes.js";
import { chunks } from "../../../io/cooperative.js";
import { closeQuietly, ensureStream } from "../../../io/stream.js";
import { IOResult, materialize } from "../../../io/types.js";
import { OnExceed } from "../../../types.js";
import { CommandTimeoutError } from "../../../errors/types.js";
const NEWLINE = 0x0a;
const ENC = new TextEncoder();
const TIMED_OUT = Symbol('timed-out');
/** The start of the notice a command's capped output ends its stderr with. */
export const TRUNCATION_NOTICE = 'output truncated at limit';
function withDeadline(promise, ms) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
resolve(TIMED_OUT);
}, ms);
promise.then((value) => {
clearTimeout(timer);
resolve(value);
}, (err) => {
clearTimeout(timer);
reject(err instanceof Error ? err : new Error(String(err)));
});
});
}
/**
* Wait for `promise` until its deadline, then throw a timeout error. The call
* itself runs on, and a stream it answers after the deadline is closed, since
* no one reads it and it may hold the backend open (an SSH follow's remote
* `tail`). `start` is when the budget began (`performance.now()`), when work
* before this call already spent part of it. Mirrors Python's
* `run_with_timeout`, whose deadline cancels the call itself.
*/
export async function runWithTimeout(promise, seconds, name, start) {
if (seconds === null || seconds <= 0)
return await promise;
const left = seconds * 1000 - (start === undefined ? 0 : performance.now() - start);
// Spent before the call: a race against no time left loses to a call that
// settles in a microtask.
const result = left > 0 ? await withDeadline(promise, left) : TIMED_OUT;
if (result === TIMED_OUT) {
void promise.then(async (late) => {
if (late !== null && typeof late === 'object' && Symbol.asyncIterator in late) {
await closeQuietly(late);
}
}, (err) => {
console.debug(`${name}: a timed-out call failed after its deadline: ${String(err)}`);
});
throw new CommandTimeoutError(name || '?', seconds);
}
return result;
}
function buildNotice(limit) {
const parts = [];
if (limit.maxLines !== null)
parts.push(`${String(limit.maxLines)} lines`);
if (limit.maxBytes !== null)
parts.push(`${String(limit.maxBytes)} bytes`);
const detail = parts.join(' / ');
return ENC.encode(`${TRUNCATION_NOTICE} (${detail}); narrow the selection or raise command_limits for this command\n`);
}
export async function* truncateStream(src, io, limit) {
const maxBytes = limit.maxBytes;
const iterable = ensureStream(src);
if (maxBytes === null) {
yield* iterable;
return;
}
let emitted = 0;
for await (const chunk of iterable) {
const remaining = maxBytes - emitted;
if (chunk.byteLength <= remaining) {
yield chunk;
emitted += chunk.byteLength;
continue;
}
if (remaining > 0)
yield chunk.subarray(0, remaining);
const existing = io.stderr !== null ? await materialize(io.stderr) : new Uint8Array();
const notice = buildNotice(limit);
const merged = new Uint8Array(existing.byteLength + notice.byteLength);
merged.set(existing, 0);
merged.set(notice, existing.byteLength);
io.stderr = merged;
if (limit.onExceed === OnExceed.ERROR)
io.exitCode = 1;
return;
}
}
async function* boundedStream(src, io, limit, command = '') {
let total = 0;
let lines = 0;
const iterable = ensureStream(src);
for await (const chunk of iterable) {
let end = limit.maxBytes === null
? chunk.byteLength
: Math.min(chunk.byteLength, Math.max(0, limit.maxBytes - total));
if (limit.maxLines !== null) {
if (lines >= limit.maxLines)
end = 0;
else {
for (let i = 0; i < end; i++) {
if (chunk[i] === NEWLINE && ++lines === limit.maxLines) {
end = i + 1;
break;
}
}
}
}
total += end;
if (end > 0)
yield chunk.subarray(0, end);
if (end < chunk.byteLength) {
const prefix = command === '' ? new Uint8Array() : ENC.encode(`${command}: `);
io.stderr = concat([await materialize(io.stderr), prefix, buildNotice(limit)]);
if (limit.onExceed === OnExceed.ERROR)
io.exitCode = 1;
return;
}
}
}
async function* errorStream(src, io, limit, command) {
const outcome = new IOResult();
const data = await materialize(boundedStream(src, outcome, limit, command));
if (outcome.stderr !== null) {
io.stderr = concat([await materialize(io.stderr), await materialize(outcome.stderr)]);
}
if (outcome.exitCode !== 0)
io.exitCode = outcome.exitCode;
else if (data.byteLength > 0)
yield data;
}
/** Attach a terminal cap; its consuming statement settles status and notices. */
export function guardIO(stdout, io, limit, command = '') {
if (stdout === null || limit === null || (limit.maxLines === null && limit.maxBytes === null))
return stdout;
return limit.onExceed === OnExceed.ERROR
? errorStream(stdout, io, limit, command)
: boundedStream(stdout, io, limit, command);
}
/**
* Apply output caps at a boundary and merge the outcome.
*
* The one boundary rule, shared by the command tree and the
* whole-line runtimes: cap stdout, append the truncation notice to
* stderr, and let an ERROR-mode guard override the exit code.
*/
export async function guardOutput(stdout, stderr, exitCode, limit) {
const io = new IOResult({ stderr, exitCode });
const guarded = guardIO(stdout, io, limit);
const data = guarded === null ? null : await materialize(guarded);
return [data, io.stderr, io.exitCode];
}
/** An invocation's input under its abort; `split` as in chunks. */
export function guardInput(source, ctx, split = true) {
return chunks(source, ctx.signal, split);
}
//# sourceMappingURL=limit.js.map