UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

145 lines • 4.74 kB
import { makeAbortError } from "../utils/abort.js"; import { concat } from "../utils/bytes.js"; import { OutputPipe } from "../io/output.js"; import { CAPACITY } from "../io/pipe.js"; import { JobConsole } from "../shell/console/job_console.js"; import { Channel } from "../shell/console/types.js"; import { PipeClosed } from "../io/errors.js"; import { ExecuteResult } from "./workspace/types.js"; class ShellOutput extends JobConsole { pipe; constructor(bufferBytes) { super(); this.pipe = new OutputPipe(bufferBytes); } async emit(channel, data) { if (channel === Channel.CONTROL) return; await this.pipe.write(channel, data); } } class ShellEvents { owner; output; source; ended = false; pulling = false; constructor(owner, output) { this.owner = owner; this.output = output; this.source = output.pipe.events(); } [Symbol.asyncIterator]() { return this; } async next() { if (this.ended) return { done: true, value: undefined }; if (this.pulling) throw new Error('shell events already have a pending reader'); this.pulling = true; try { const next = await this.source.next(); if (this.output.pipe.closedReader || next.done) { this.ended = true; return { done: true, value: undefined }; } return next; } catch (error) { this.owner.cancel(); throw error; } finally { this.pulling = false; } } async return() { await this.owner.close(); return { done: true, value: undefined }; } async release() { this.ended = true; this.output.pipe.closeReader(); await this.source.return(undefined); } } /** A shell invocation and bounded single-reader events. Drain events before wait(). */ export class ShellExecution { events; controller = new AbortController(); output; result; closing; settled = false; constructor(run, scope, signal, bufferBytes = CAPACITY) { this.output = new ShellOutput(bufferBytes); this.events = new ShellEvents(this, this.output); const combined = signal === undefined ? this.controller.signal : AbortSignal.any([signal, this.controller.signal]); this.result = this.run(run, scope, combined); // The outcome is delivered by wait()/events even when a consumer closes before its first pull. void this.result.catch(() => undefined); } async run(run, scope, signal) { try { if (signal.aborted) throw makeAbortError(signal); const result = await run(this.output, signal, scope); this.output.pipe.end(); return result; } catch (error) { const failure = signal.aborted && error instanceof PipeClosed ? makeAbortError(signal) : error; this.output.pipe.end(failure); throw failure; } finally { this.settled = true; } } /** @internal Release workspace registration when invocation settles. */ onSettled(callback) { void this.result.then(callback, callback); } cancel() { if (!this.settled) { this.controller.abort(new DOMException('execute aborted', 'AbortError')); this.output.pipe.end(this.controller.signal.reason); } } /** Join the invocation; final stdout/stderr are empty because events delivered them. */ wait() { return this.result; } /** Collect output and await an optional observer before the next pull. */ async collect(onEvent) { const stdout = []; const stderr = []; try { for await (const event of this.events) { ; (event.stream === Channel.STDOUT ? stdout : stderr).push(event.data); await onEvent?.(event); } const result = await this.wait(); return new ExecuteResult(concat(stdout), concat(stderr), result.exitCode, result.refusal); } finally { await this.close(); } } /** Discard unread output, cancel and join under the shell's normal cancellation policy. */ close() { this.closing ??= this.closeOwned(); return this.closing; } async closeOwned() { this.cancel(); await this.events.release(); await Promise.allSettled([this.result]); await this.output.close(); } } //# sourceMappingURL=shell_execution.js.map