diff --git a/host/js/p4-test-bundle.js b/host/js/p4-test-bundle.js deleted file mode 100644 index b73958c..0000000 --- a/host/js/p4-test-bundle.js +++ /dev/null @@ -1,1730 +0,0 @@ -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __commonJS = (cb, mod) => function __require() { - try { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; - } catch (e) { - throw mod = 0, e; - } -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); - -// node_modules/partial-json/dist/options.js -var require_options = __commonJS({ - "node_modules/partial-json/dist/options.js"(exports) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.Allow = exports.ALL = exports.COLLECTION = exports.ATOM = exports.SPECIAL = exports.INF = exports._INFINITY = exports.INFINITY = exports.NAN = exports.BOOL = exports.NULL = exports.OBJ = exports.ARR = exports.NUM = exports.STR = void 0; - exports.STR = 1; - exports.NUM = 2; - exports.ARR = 4; - exports.OBJ = 8; - exports.NULL = 16; - exports.BOOL = 32; - exports.NAN = 64; - exports.INFINITY = 128; - exports._INFINITY = 256; - exports.INF = exports.INFINITY | exports._INFINITY; - exports.SPECIAL = exports.NULL | exports.BOOL | exports.INF | exports.NAN; - exports.ATOM = exports.STR | exports.NUM | exports.SPECIAL; - exports.COLLECTION = exports.ARR | exports.OBJ; - exports.ALL = exports.ATOM | exports.COLLECTION; - exports.Allow = { STR: exports.STR, NUM: exports.NUM, ARR: exports.ARR, OBJ: exports.OBJ, NULL: exports.NULL, BOOL: exports.BOOL, NAN: exports.NAN, INFINITY: exports.INFINITY, _INFINITY: exports._INFINITY, INF: exports.INF, SPECIAL: exports.SPECIAL, ATOM: exports.ATOM, COLLECTION: exports.COLLECTION, ALL: exports.ALL }; - exports.default = exports.Allow; - } -}); - -// node_modules/partial-json/dist/index.js -var require_dist = __commonJS({ - "node_modules/partial-json/dist/index.js"(exports) { - "use strict"; - var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) { - if (k2 === void 0) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { - return m[k]; - } }; - } - Object.defineProperty(o, k2, desc); - }) : (function(o, m, k, k2) { - if (k2 === void 0) k2 = k; - o[k2] = m[k]; - })); - var __exportStar = exports && exports.__exportStar || function(m, exports2) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p); - }; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.Allow = exports.MalformedJSON = exports.PartialJSON = exports.parseJSON = exports.parse = void 0; - var options_1 = require_options(); - Object.defineProperty(exports, "Allow", { enumerable: true, get: function() { - return options_1.Allow; - } }); - __exportStar(require_options(), exports); - var PartialJSON = class extends Error { - }; - exports.PartialJSON = PartialJSON; - var MalformedJSON = class extends Error { - }; - exports.MalformedJSON = MalformedJSON; - function parseJSON(jsonString, allowPartial = options_1.Allow.ALL) { - if (typeof jsonString !== "string") { - throw new TypeError(`expecting str, got ${typeof jsonString}`); - } - if (!jsonString.trim()) { - throw new Error(`${jsonString} is empty`); - } - return _parseJSON(jsonString.trim(), allowPartial); - } - exports.parseJSON = parseJSON; - var _parseJSON = (jsonString, allow) => { - const length = jsonString.length; - let index = 0; - const markPartialJSON = (msg) => { - throw new PartialJSON(`${msg} at position ${index}`); - }; - const throwMalformedError = (msg) => { - throw new MalformedJSON(`${msg} at position ${index}`); - }; - const parseAny = () => { - skipBlank(); - if (index >= length) - markPartialJSON("Unexpected end of input"); - if (jsonString[index] === '"') - return parseStr(); - if (jsonString[index] === "{") - return parseObj(); - if (jsonString[index] === "[") - return parseArr(); - if (jsonString.substring(index, index + 4) === "null" || options_1.Allow.NULL & allow && length - index < 4 && "null".startsWith(jsonString.substring(index))) { - index += 4; - return null; - } - if (jsonString.substring(index, index + 4) === "true" || options_1.Allow.BOOL & allow && length - index < 4 && "true".startsWith(jsonString.substring(index))) { - index += 4; - return true; - } - if (jsonString.substring(index, index + 5) === "false" || options_1.Allow.BOOL & allow && length - index < 5 && "false".startsWith(jsonString.substring(index))) { - index += 5; - return false; - } - if (jsonString.substring(index, index + 8) === "Infinity" || options_1.Allow.INFINITY & allow && length - index < 8 && "Infinity".startsWith(jsonString.substring(index))) { - index += 8; - return Infinity; - } - if (jsonString.substring(index, index + 9) === "-Infinity" || options_1.Allow._INFINITY & allow && 1 < length - index && length - index < 9 && "-Infinity".startsWith(jsonString.substring(index))) { - index += 9; - return -Infinity; - } - if (jsonString.substring(index, index + 3) === "NaN" || options_1.Allow.NAN & allow && length - index < 3 && "NaN".startsWith(jsonString.substring(index))) { - index += 3; - return NaN; - } - return parseNum(); - }; - const parseStr = () => { - const start = index; - let escape = false; - index++; - while (index < length && (jsonString[index] !== '"' || escape && jsonString[index - 1] === "\\")) { - escape = jsonString[index] === "\\" ? !escape : false; - index++; - } - if (jsonString.charAt(index) == '"') { - try { - return JSON.parse(jsonString.substring(start, ++index - Number(escape))); - } catch (e) { - throwMalformedError(String(e)); - } - } else if (options_1.Allow.STR & allow) { - try { - return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"'); - } catch (e) { - return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"'); - } - } - markPartialJSON("Unterminated string literal"); - }; - const parseObj = () => { - index++; - skipBlank(); - const obj = {}; - try { - while (jsonString[index] !== "}") { - skipBlank(); - if (index >= length && options_1.Allow.OBJ & allow) - return obj; - const key = parseStr(); - skipBlank(); - index++; - try { - const value = parseAny(); - obj[key] = value; - } catch (e) { - if (options_1.Allow.OBJ & allow) - return obj; - else - throw e; - } - skipBlank(); - if (jsonString[index] === ",") - index++; - } - } catch (e) { - if (options_1.Allow.OBJ & allow) - return obj; - else - markPartialJSON("Expected '}' at end of object"); - } - index++; - return obj; - }; - const parseArr = () => { - index++; - const arr = []; - try { - while (jsonString[index] !== "]") { - arr.push(parseAny()); - skipBlank(); - if (jsonString[index] === ",") { - index++; - } - } - } catch (e) { - if (options_1.Allow.ARR & allow) { - return arr; - } - markPartialJSON("Expected ']' at end of array"); - } - index++; - return arr; - }; - const parseNum = () => { - if (index === 0) { - if (jsonString === "-") - throwMalformedError("Not sure what '-' is"); - try { - return JSON.parse(jsonString); - } catch (e) { - if (options_1.Allow.NUM & allow) - try { - return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf("e"))); - } catch (e2) { - } - throwMalformedError(String(e)); - } - } - const start = index; - if (jsonString[index] === "-") - index++; - while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1) - index++; - if (index == length && !(options_1.Allow.NUM & allow)) - markPartialJSON("Unterminated number literal"); - try { - return JSON.parse(jsonString.substring(start, index)); - } catch (e) { - if (jsonString.substring(start, index) === "-") - markPartialJSON("Not sure what '-' is"); - try { - return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("e"))); - } catch (e2) { - throwMalformedError(String(e2)); - } - } - }; - const skipBlank = () => { - while (index < length && " \n\r ".includes(jsonString[index])) { - index++; - } - }; - return parseAny(); - }; - var parse = parseJSON; - exports.parse = parse; - } -}); - -// node_modules/@earendil-works/pi-ai/dist/utils/event-stream.js -var FifoQueue = class { - incoming = []; - outgoing = []; - get length() { - return this.incoming.length + this.outgoing.length; - } - enqueue(value) { - this.incoming.push(value); - } - dequeue() { - if (this.outgoing.length === 0) { - while (this.incoming.length > 0) { - this.outgoing.push(this.incoming.pop()); - } - } - return this.outgoing.pop(); - } -}; -var EventStream = class { - queue = new FifoQueue(); - waiting = new FifoQueue(); - done = false; - finalResultPromise; - resolveFinalResult; - isComplete; - extractResult; - constructor(isComplete, extractResult) { - this.isComplete = isComplete; - this.extractResult = extractResult; - this.finalResultPromise = new Promise((resolve) => { - this.resolveFinalResult = resolve; - }); - } - push(event) { - if (this.done) - return; - if (this.isComplete(event)) { - this.done = true; - this.resolveFinalResult(this.extractResult(event)); - } - const waiter = this.waiting.dequeue(); - if (waiter) { - waiter({ value: event, done: false }); - } else { - this.queue.enqueue(event); - } - } - end(result) { - this.done = true; - if (result !== void 0) { - this.resolveFinalResult(result); - } - while (this.waiting.length > 0) { - const waiter = this.waiting.dequeue(); - waiter({ value: void 0, done: true }); - } - } - async *[Symbol.asyncIterator]() { - while (true) { - if (this.queue.length > 0) { - yield this.queue.dequeue(); - } else if (this.done) { - return; - } else { - const result = await new Promise((resolve) => this.waiting.enqueue(resolve)); - if (result.done) - return; - yield result.value; - } - } - } - result() { - return this.finalResultPromise; - } -}; -var AssistantMessageEventStream = class extends EventStream { - #startedAt = Date.now(); - #startedAtMonotonic = performance.now(); - constructor() { - super((event) => event.type === "done" || event.type === "error", (event) => { - if (event.type === "done") { - return event.message; - } else if (event.type === "error") { - return event.error; - } - throw new Error("Unexpected event type for final result"); - }); - } - push(event) { - if (event.type === "done") - this.#time(event.message); - else if (event.type === "error") - this.#time(event.error); - super.push(event); - } - end(result) { - if (result !== void 0) - this.#time(result); - super.end(result); - } - #time(message) { - if (this.done || message.durationMs !== void 0 || message.timestamp < this.#startedAt) - return; - message.durationMs = Math.max(0, Math.round(performance.now() - this.#startedAtMonotonic)); - } -}; -function createAssistantMessageEventStream() { - return new AssistantMessageEventStream(); -} - -// node_modules/@earendil-works/pi-ai/dist/api/lazy.js -function createSetupErrorMessage(model, error, timestamp) { - return { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } - }, - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp - }; -} -function hasResult(source) { - return typeof source.result === "function"; -} -async function forwardStream(target, source, observer, observeResult) { - let result; - for await (const event of source) { - target.push(event); - observer?.onEvent?.(event); - if (event.type === "done") - result = event.message; - else if (event.type === "error") - result = event.error; - } - if (hasResult(source)) - result = await source.result(); - target.end(result); - observeResult(result); -} -function lazyStream(model, setup, observer) { - const startedAt = Date.now(); - const outer = new AssistantMessageEventStream(); - let observedResult = false; - const observeResult = (result) => { - if (observedResult) - return; - observedResult = true; - observer?.onResult?.(result); - }; - setup().then((inner) => forwardStream(outer, inner, observer, observeResult)).catch((error) => { - const message = createSetupErrorMessage(model, error, startedAt); - const event = { type: "error", reason: "error", error: message }; - outer.push(event); - observer?.onEvent?.(event); - outer.end(message); - observeResult(message); - }); - return outer; -} - -// node_modules/@earendil-works/pi-ai/dist/utils/diagnostics.js -function formatThrownValue(value) { - if (value instanceof Error) - return value.message || value.name; - if (typeof value === "string") - return value; - return String(value); -} - -// node_modules/@earendil-works/pi-ai/dist/utils/models-error.js -var ModelsError = class extends Error { - code; - constructor(code, message, options) { - super(withCauseDetail(message, options?.cause), options); - this.name = "ModelsError"; - this.code = code; - } -}; -function withCauseDetail(message, cause) { - if (cause === void 0 || cause === null) - return message; - const detail = formatThrownValue(cause).trim(); - if (!detail || message.includes(detail)) - return message; - return `${message}: ${detail}`; -} - -// node_modules/@earendil-works/pi-ai/dist/auth/resolve.js -var DEFAULT_OAUTH_MINIMUM_VALIDITY_MS = 5 * 60 * 1e3; - -// node_modules/@earendil-works/pi-ai/dist/utils/text.js -function contentText(content, separator = "\n") { - if (typeof content === "string") - return content; - return content.filter((block) => block.type === "text").map((block) => block.text).join(separator); -} -function getSystemMessageText(message) { - const parts = [contentText(message.content)]; - for (const text of Object.values(message.sections ?? {})) { - if (text !== null) - parts.push(text); - } - return parts.filter((part) => part.length > 0).join("\n\n"); -} - -// node_modules/@earendil-works/pi-ai/dist/utils/model-operations.js -function getModelType(model) { - return model.type ?? "chat"; -} -function isModelType(model, type) { - return getModelType(model) === type; -} -function imageErrorResult(model, error, aborted = false) { - return { - api: model.api, - provider: model.provider, - model: model.id, - output: [], - stopReason: aborted ? "aborted" : "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now() - }; -} -function classifierErrorResult(model, error, aborted = false) { - return { - api: model.api, - provider: model.provider, - model: model.id, - answers: {}, - stopReason: aborted ? "aborted" : "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now() - }; -} - -// node_modules/@earendil-works/pi-ai/dist/models.js -var KNOWN_MODEL_TYPES = { chat: true, image: true, classifier: true }; -function hasKnownModelType(model) { - return Object.hasOwn(KNOWN_MODEL_TYPES, getModelType(model)); -} -function createProvider(input) { - const single = input.api && typeof input.api.stream === "function" ? input.api : void 0; - const byApi = single || !input.api ? void 0 : input.api; - const images = input.images; - const classifiers = input.classifiers; - const streams = single ? [single] : Object.values(byApi ?? {}).filter((entry) => entry !== void 0); - const imageImplementations = Object.values(images ?? {}).filter((entry) => entry !== void 0); - const classifierImplementations = Object.values(classifiers ?? {}).filter((entry) => entry !== void 0); - if (streams.length === 0 && imageImplementations.length === 0 && classifierImplementations.length === 0) { - throw new Error(`Provider ${input.id}: at least one of "api", "images", or "classifiers" is required.`); - } - const baselineModels = input.models; - let dynamicModels = []; - const fetchModels = input.fetchModels; - const currentModels = () => { - const merged = [...baselineModels]; - for (const model of dynamicModels) { - const index = merged.findIndex((entry) => getModelType(entry) === getModelType(model) && entry.id === model.id); - if (index >= 0) - merged[index] = model; - else - merged.push(model); - } - return merged; - }; - const apiFor = (model) => single ?? byApi?.[model.api]; - const dispatch = (model, run) => { - const streams2 = apiFor(model); - if (!streams2) { - return lazyStream(model, async () => { - throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model.api}"`); - }); - } - return run(streams2); - }; - const provider = { - id: input.id, - name: input.name ?? input.id, - baseUrl: input.baseUrl, - headers: input.headers, - auth: input.auth, - getModels: () => currentModels().filter((model) => isModelType(model, "chat")), - getAllModels: currentModels, - refreshModels: fetchModels ? async (context) => { - if (context.stored) { - const restored = context.stored.models.filter((model) => model.provider === input.id).map((model) => model); - if (!await context.publish({ - update: () => { - dynamicModels = restored; - } - })) { - return; - } - } - if (!context.allowNetwork || context.signal.aborted) - return; - const fetched = await fetchModels(context); - if (context.signal.aborted) - return; - const refreshed = fetched.filter(hasKnownModelType); - await context.publish({ - persist: { models: refreshed, checkedAt: Date.now() }, - update: () => { - dynamicModels = refreshed; - } - }); - } : void 0, - filterModels: input.filterModels, - filterAllModels: input.filterAllModels, - stream: (model, context, options) => dispatch(model, (streams2) => streams2.stream(model, context, options)), - streamSimple: (model, context, options) => dispatch(model, (streams2) => streams2.streamSimple(model, context, options)) - }; - if (streams.some((entry) => entry.fetchDeferred !== void 0)) { - provider.fetchDeferred = (model, handle, options) => lazyStream(model, async () => { - const implementation = apiFor(model); - if (!implementation?.fetchDeferred) { - throw new ModelsError("provider", `Provider ${input.id} does not support deferred responses for "${model.api}"`); - } - return implementation.fetchDeferred(model, handle, options); - }); - } - if (streams.some((entry) => entry.cancelDeferred !== void 0)) { - provider.cancelDeferred = async (model, handle, options) => { - const implementation = apiFor(model); - if (!implementation?.cancelDeferred) { - throw new ModelsError("provider", `Provider ${input.id} cannot cancel deferred responses for "${model.api}"`); - } - await implementation.cancelDeferred(model, handle, options); - }; - } - if (images && imageImplementations.length > 0) { - provider.generateImages = async (model, context, options) => { - const implementation = images[model.api]; - if (!implementation) { - return imageErrorResult(model, new ModelsError("provider", `Provider ${input.id} has no image generation implementation for "${model.api}"`)); - } - return implementation.generateImages(model, context, options); - }; - } - if (classifiers && classifierImplementations.length > 0) { - provider.classify = async (model, context, options) => { - const implementation = classifiers[model.api]; - if (!implementation) { - return classifierErrorResult(model, new ModelsError("provider", `Provider ${input.id} has no classifier implementation for "${model.api}"`)); - } - return implementation.classify(model, context, options); - }; - } - return provider; -} - -// node_modules/@earendil-works/pi-ai/dist/utils/json-parse.js -var import_partial_json = __toESM(require_dist(), 1); -var VALID_JSON_ESCAPES = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t"]); -function isControlCharacter(char) { - const codePoint = char.codePointAt(0); - return codePoint !== void 0 && codePoint >= 0 && codePoint <= 31; -} -function escapeControlCharacter(char) { - switch (char) { - case "\b": - return "\\b"; - case "\f": - return "\\f"; - case "\n": - return "\\n"; - case "\r": - return "\\r"; - case " ": - return "\\t"; - default: - return `\\u${char.codePointAt(0)?.toString(16).padStart(4, "0") ?? "0000"}`; - } -} -function repairJson(json) { - let repaired = ""; - let inString = false; - for (let index = 0; index < json.length; index++) { - const char = json[index]; - if (!inString) { - repaired += char; - if (char === '"') { - inString = true; - } - continue; - } - if (char === '"') { - repaired += char; - inString = false; - continue; - } - if (char === "\\") { - const nextChar = json[index + 1]; - if (nextChar === void 0) { - repaired += "\\\\"; - continue; - } - if (nextChar === "u") { - const unicodeDigits = json.slice(index + 2, index + 6); - if (/^[0-9a-fA-F]{4}$/.test(unicodeDigits)) { - repaired += `\\u${unicodeDigits}`; - index += 5; - continue; - } - } - if (VALID_JSON_ESCAPES.has(nextChar)) { - repaired += `\\${nextChar}`; - index += 1; - continue; - } - repaired += "\\\\"; - continue; - } - repaired += isControlCharacter(char) ? escapeControlCharacter(char) : char; - } - return repaired; -} -function parseJsonWithRepair(json) { - try { - return JSON.parse(json); - } catch (error) { - const repairedJson = repairJson(json); - if (repairedJson !== json) { - return JSON.parse(repairedJson); - } - throw error; - } -} -function parseToolCallArguments(json) { - if (!json?.trim()) - return { arguments: {} }; - try { - const parsed = parseJsonWithRepair(json); - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { - throw new Error("Tool arguments must be a JSON object"); - } - return { arguments: parsed }; - } catch (error) { - return { - arguments: {}, - argumentsError: `${error instanceof Error ? error.message : String(error)} - -Received arguments: -${json}` - }; - } -} - -// node_modules/@earendil-works/pi-ai/dist/utils/streaming-json.js -var MAX_REPORTED_DEPTH = 32; -var ROOT_SLOT = { kind: "root" }; -var CREDIT_PER_APPEND = 256; -var CREDIT_PER_CHARACTER = 8; -var MAX_PENDING_SCALAR_LENGTH = 64; -var NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; -var NUMBER_WITHOUT_EXPONENT_DIGITS = /^(-?(?:0|[1-9]\d*)(?:\.\d+)?)[eE][+-]?$/; -var LITERALS = [ - ["true", true], - ["false", false], - ["null", null] -]; -var QUOTE = 34; -var BACKSLASH = 92; -var StreamingJsonParser = class { - #root = void 0; - #done = false; - #stack = []; - #string = void 0; - #scalar = ""; - #scalarSlot = void 0; - /** Sum of `size + 1` over the open path: what a fresh snapshot copies. */ - #pathCost = 0; - #credit = 0; - #dirty = false; - #snapshot = void 0; - /** Changes of the piece being read. */ - #changes = []; - /** - * Read the next piece of text; returns the changes it made, in order. Costs time linear in `text.length`. - */ - append(text) { - this.#credit += CREDIT_PER_APPEND + CREDIT_PER_CHARACTER * text.length; - this.#changes = []; - if (this.#string !== void 0) - this.#string.change = void 0; - let index = 0; - while (index < text.length && !this.#done) { - const open = this.#string; - if (open !== void 0) - index = this.#readString(open, text, index); - else if (this.#scalar !== "") - index = this.#readScalar(text, index); - else - this.#structural(text[index++]); - } - return this.#changes; - } - /** - * The value of the text read so far: `undefined` before a value starts, otherwise a plain JSON value that never - * changes afterwards. Returns the previous snapshot while the text read since did not change the value, or while a - * fresh copy of the open path is not yet paid for (see the module comment). - */ - snapshot() { - if (!this.#dirty) - return this.#snapshot; - const pending = this.#pendingScalar(); - const cost = this.#pathCost + (pending === void 0 ? 0 : 1); - if (cost > this.#credit) - return this.#snapshot; - this.#credit -= cost; - this.#dirty = false; - this.#snapshot = this.#copyOpenPath(pending); - return this.#snapshot; - } - /** Read string characters from `start` until the string ends or the text does; returns the next index. */ - #readString(open, text, start) { - let runStart = start; - for (let index = start; index < text.length; index++) { - const code = text.charCodeAt(index); - if (open.escape !== "") { - if (this.#continueEscape(open, text[index])) { - runStart = index + 1; - continue; - } - runStart = index; - } - if (code === QUOTE) { - if (index > runStart) - this.#appendString(open, text.slice(runStart, index)); - this.#endString(open); - return index + 1; - } - if (code === BACKSLASH) { - if (index > runStart) - this.#appendString(open, text.slice(runStart, index)); - open.escape = "\\"; - runStart = index + 1; - } - } - if (text.length > runStart) - this.#appendString(open, text.slice(runStart)); - return text.length; - } - /** Read number or literal characters from `start` up to the next delimiter; returns the delimiter's index. */ - #readScalar(text, start) { - let end = start; - while (end < text.length && !isDelimiter(text[end])) - end++; - if (end > start) { - this.#scalar += text.slice(start, end); - if (this.#scalarSlot !== void 0) - this.#dirty = true; - } - if (end < text.length) - this.#endScalar(); - return end; - } - /** Handle one character outside strings. */ - #structural(char) { - switch (char) { - case " ": - case " ": - case "\n": - case "\r": - return; - case '"': - this.#startString(); - return; - case "{": - case "[": - this.#openContainer(char === "{" ? {} : []); - return; - case "}": - case "]": - this.#closeContainer(); - return; - case ":": { - const frame = this.#stack.at(-1); - if (frame !== void 0 && !Array.isArray(frame.container) && frame.phase === "colon") - frame.phase = "value"; - return; - } - case ",": { - const frame = this.#stack.at(-1); - if (frame !== void 0) - frame.phase = Array.isArray(frame.container) ? "value" : "key"; - return; - } - default: - this.#scalar = char; - this.#scalarSlot = this.#valueSlot(); - if (this.#scalarSlot !== void 0) - this.#dirty = true; - } - } - /** Where a value starting now goes, or `undefined` when this position takes no value. */ - #valueSlot() { - const frame = this.#stack.at(-1); - if (frame === void 0) - return this.#root === void 0 ? ROOT_SLOT : void 0; - if (Array.isArray(frame.container)) - return { kind: "element", index: frame.container.length }; - if ((frame.phase === "value" || frame.phase === "colon") && frame.key !== void 0) { - return { kind: "member", key: frame.key }; - } - return void 0; - } - #startString() { - const frame = this.#stack.at(-1); - if (frame !== void 0 && !Array.isArray(frame.container) && (frame.phase === "key" || frame.phase === "comma")) { - this.#string = { target: "key", slot: void 0, text: "", escape: "", path: void 0, change: void 0 }; - return; - } - const slot = this.#valueSlot(); - const path = this.#reportedPath(slot); - this.#string = { target: "value", slot, text: "", escape: "", path, change: void 0 }; - this.#place(slot, ""); - if (path !== void 0) - this.#string.change = this.#report({ kind: "set", path, value: "" }); - } - /** The path of a value at `slot` in the innermost open container, when its changes are reported. */ - #reportedPath(slot) { - if (slot === void 0) - return void 0; - if (slot.kind === "root") - return []; - const frame = this.#stack.at(-1); - if (!frame.reportsInside) - return void 0; - return [...frame.path, slot.kind === "member" ? slot.key : slot.index]; - } - #report(change) { - this.#changes.push(change); - return change; - } - /** Continue an escape with `char`; returns false when `char` is not part of it and must be read normally. */ - #continueEscape(open, char) { - if (open.escape === "\\") { - const decoded = SIMPLE_ESCAPES[char]; - if (decoded !== void 0) { - open.escape = ""; - this.#appendString(open, decoded); - return true; - } - if (char === "u") { - open.escape = "\\u"; - return true; - } - open.escape = ""; - this.#appendString(open, "\\"); - return false; - } - if (isHexDigit(char)) { - open.escape += char; - if (open.escape.length < 6) - return true; - const decoded = String.fromCharCode(Number.parseInt(open.escape.slice(2), 16)); - open.escape = ""; - this.#appendString(open, decoded); - return true; - } - const literal = open.escape; - open.escape = ""; - this.#appendString(open, literal); - return false; - } - #appendString(open, text) { - open.text += text; - if (open.target !== "value") - return; - this.#place(open.slot, open.text); - if (open.path === void 0) - return; - const change = open.change; - if (change !== void 0 && this.#changes.at(-1) === change) { - if (change.kind === "set") - change.value = change.value + text; - else - change.text += text; - return; - } - open.change = this.#report({ kind: "append", path: open.path, text }); - } - #endString(open) { - this.#string = void 0; - const frame = this.#stack.at(-1); - if (open.target === "key") { - if (frame === void 0) - return; - frame.key = open.text; - frame.phase = "colon"; - return; - } - this.#valueEnded(frame); - } - #endScalar() { - const token = this.#scalar; - const slot = this.#scalarSlot; - this.#scalar = ""; - this.#scalarSlot = void 0; - const value = scalarValue(token); - if (value !== void 0) { - this.#place(slot, value); - const path = this.#reportedPath(slot); - if (path !== void 0) - this.#report({ kind: "set", path, value }); - } - this.#valueEnded(this.#stack.at(-1)); - } - /** The value of the unterminated scalar at the end of the text, if it shows. */ - #pendingScalar() { - const slot = this.#scalarSlot; - const token = this.#scalar; - if (slot === void 0 || token === "" || token.length > MAX_PENDING_SCALAR_LENGTH) - return void 0; - const value = pendingScalarValue(token); - return value === void 0 ? void 0 : { slot, value }; - } - #openContainer(container) { - const slot = this.#valueSlot(); - const path = this.#reportedPath(slot); - this.#place(slot, container); - this.#stack.push({ - container, - slot, - path, - reportsInside: path !== void 0 && path.length < MAX_REPORTED_DEPTH, - size: 0, - key: void 0, - phase: Array.isArray(container) ? "value" : "key" - }); - this.#pathCost += 1; - this.#dirty = true; - if (path !== void 0) - this.#report({ kind: "set", path, value: Array.isArray(container) ? [] : {} }); - } - #closeContainer() { - const frame = this.#stack.pop(); - if (frame === void 0) - return; - this.#pathCost -= frame.size + 1; - this.#dirty = true; - if (frame.path !== void 0 && !frame.reportsInside) { - this.#report({ kind: "set", path: frame.path, value: frame.container }); - } - this.#valueEnded(this.#stack.at(-1)); - } - /** A value in `frame`, or the root value when there is no frame, has ended. */ - #valueEnded(frame) { - if (frame === void 0) { - if (this.#root !== void 0) - this.#done = true; - return; - } - frame.phase = "comma"; - } - /** Set the value at `slot` in the open path; nothing for a value in an invalid position. */ - #place(slot, value) { - if (slot === void 0) - return; - this.#dirty = true; - if (slot.kind === "root") { - this.#root = value; - return; - } - const frame = this.#stack.at(-1); - if (slot.kind === "element") { - const elements = frame.container; - if (slot.index === elements.length) { - frame.size++; - this.#pathCost++; - } - elements[slot.index] = value; - return; - } - const members = frame.container; - if (!Object.hasOwn(members, slot.key)) { - frame.size++; - this.#pathCost++; - } - setMember(members, slot.key, value); - } - /** A fresh snapshot: copies of the open containers, innermost first, each holding the copy of its open child. */ - #copyOpenPath(pending) { - if (this.#stack.length === 0) - return pending?.slot.kind === "root" ? pending.value : this.#root; - let child; - let childSlot; - for (let depth = this.#stack.length - 1; depth >= 0; depth--) { - const frame = this.#stack[depth]; - const copy = Array.isArray(frame.container) ? frame.container.slice() : { ...frame.container }; - if (depth === this.#stack.length - 1 && pending !== void 0) - setSlot(copy, pending.slot, pending.value); - if (child !== void 0 && childSlot !== void 0) - setSlot(copy, childSlot, child); - child = copy; - childSlot = frame.slot; - } - return childSlot?.kind === "root" ? child : this.#root; - } -}; -var ToolCallArgumentsStream = class { - #text = ""; - #parser = new StreamingJsonParser(); - #snapshot = void 0; - #arguments = {}; - /** The raw argument text received so far. */ - get text() { - return this.#text; - } - /** Append a piece of argument text; returns the partial arguments, a new object whenever they changed. */ - append(delta) { - this.#text += delta; - this.#parser.append(delta); - return this.#partial(); - } - /** - * Take the provider's complete argument text. When it continues the text so far, returns the missing piece as - * `delta`; otherwise starts over with `text`. Either way returns the partial arguments of `text`. - */ - replace(text) { - if (text.startsWith(this.#text)) { - const delta = text.slice(this.#text.length); - return { delta, arguments: delta === "" ? this.#partial() : this.append(delta) }; - } - this.#text = ""; - this.#parser = new StreamingJsonParser(); - return { delta: void 0, arguments: this.append(text) }; - } - /** The final arguments: one full parse of the complete text, which reports malformed arguments. */ - finish() { - return parseToolCallArguments(this.#text); - } - #partial() { - const snapshot = this.#parser.snapshot(); - if (snapshot === this.#snapshot) - return this.#arguments; - this.#snapshot = snapshot; - this.#arguments = typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot) ? snapshot : {}; - return this.#arguments; - } -}; -var SIMPLE_ESCAPES = { - '"': '"', - "\\": "\\", - "/": "/", - b: "\b", - f: "\f", - n: "\n", - r: "\r", - t: " " -}; -function isDelimiter(char) { - return char === " " || char === " " || char === "\n" || char === "\r" || char === "," || char === ":" || char === "{" || char === "}" || char === "[" || char === "]" || char === '"'; -} -function isHexDigit(char) { - return char >= "0" && char <= "9" || char >= "a" && char <= "f" || char >= "A" && char <= "F"; -} -function scalarValue(token) { - for (const [literal, value] of LITERALS) - if (token === literal) - return value; - return NUMBER.test(token) ? Number(token) : void 0; -} -function pendingScalarValue(token) { - for (const [literal, value] of LITERALS) - if (literal.startsWith(token)) - return value; - if (NUMBER.test(token)) - return Number(token); - const mantissa = NUMBER_WITHOUT_EXPONENT_DIGITS.exec(token); - return mantissa === null ? void 0 : Number(mantissa[1]); -} -function setSlot(container, slot, value) { - if (slot.kind === "element") - container[slot.index] = value; - else if (slot.kind === "member") - setMember(container, slot.key, value); -} -function setMember(members, key, value) { - if (key === "__proto__") { - Object.defineProperty(members, key, { value, writable: true, enumerable: true, configurable: true }); - } else { - members[key] = value; - } -} - -// node_modules/@earendil-works/pi-ai/dist/providers/faux.js -var DEFAULT_API = "faux"; -var DEFAULT_PROVIDER = "faux"; -var DEFAULT_MODEL_ID = "faux-1"; -var DEFAULT_MODEL_NAME = "Faux Model"; -var DEFAULT_BASE_URL = "http://localhost:0"; -var DEFAULT_MIN_TOKEN_SIZE = 3; -var DEFAULT_MAX_TOKEN_SIZE = 5; -var DEFAULT_USAGE = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } -}; -function fauxText(text) { - return { type: "text", text }; -} -function fauxToolCall(name, arguments_, options = {}) { - return { - type: "toolCall", - id: options.id ?? randomId("tool"), - name, - arguments: arguments_ - }; -} -function normalizeFauxAssistantContent(content) { - if (typeof content === "string") { - return [fauxText(content)]; - } - return Array.isArray(content) ? content : [content]; -} -function fauxAssistantMessage(content, options = {}) { - return { - role: "assistant", - content: normalizeFauxAssistantContent(content), - api: DEFAULT_API, - provider: DEFAULT_PROVIDER, - model: DEFAULT_MODEL_ID, - usage: DEFAULT_USAGE, - stopReason: options.stopReason ?? "stop", - ...options.deferred === void 0 ? {} : { deferred: options.deferred }, - ...options.errorMessage === void 0 ? {} : { errorMessage: options.errorMessage }, - ...options.responseId === void 0 ? {} : { responseId: options.responseId }, - timestamp: options.timestamp ?? Date.now() - }; -} -function estimateTokens(text) { - return Math.ceil(text.length / 4); -} -function randomId(prefix) { - return `${prefix}:${Date.now()}:${Math.random().toString(36).slice(2)}`; -} -function contentToText(content) { - if (typeof content === "string") { - return content; - } - return content.map((block) => { - if (block.type === "text") { - return block.text; - } - return `[image:${block.mimeType}:${block.data.length}]`; - }).join("\n"); -} -function assistantContentToText(content) { - return content.map((block) => { - if (block.type === "text") { - return block.text; - } - if (block.type === "thinking") { - return block.thinking; - } - return `${block.name}:${JSON.stringify(block.arguments)}`; - }).join("\n"); -} -function toolResultToText(message) { - return [message.toolName, ...message.content.map((block) => contentToText([block]))].join("\n"); -} -function messageToText(message) { - if (message.role === "system") { - return [ - getSystemMessageText(message), - ...message.toolsRemoved?.map((tool) => `tool-:${JSON.stringify(tool)}`) ?? [], - ...message.toolsAdded?.map((tool) => `tool+:${JSON.stringify(tool)}`) ?? [] - ].filter((part) => part.length > 0).join("\n"); - } - if (message.role === "user") { - return contentToText(message.content); - } - if (message.role === "assistant") { - return assistantContentToText(message.content); - } - return toolResultToText(message); -} -function joinedLength(messages, count = messages.length) { - let length = count > 0 ? (count - 1) * 2 : 0; - for (let index = 0; index < count; index++) - length += messages[index].length; - return length; -} -function commonPromptPrefixLength(previous, current) { - let index = 0; - while (index < previous.length && index < current.length && previous[index] === current[index]) - index++; - const rest = (messages) => index === messages.length ? "" : (index > 0 ? "\n\n" : "") + messages.slice(index).join("\n\n"); - return joinedLength(previous, index) + commonPrefixLength(rest(previous), rest(current)); -} -function commonPrefixLength(a, b) { - const length = Math.min(a.length, b.length); - let index = 0; - while (index < length && a[index] === b[index]) { - index++; - } - return index; -} -function withUsageEstimate(message, context, options, promptCache) { - const prompt = context.messages.map((message2) => `${message2.role}:${messageToText(message2)}`); - const promptLength = joinedLength(prompt); - const promptTokens = Math.ceil(promptLength / 4); - const outputTokens = estimateTokens(assistantContentToText(message.content)); - let input = promptTokens; - let cacheRead = 0; - let cacheWrite = 0; - const sessionId = options?.sessionId; - if (sessionId && options?.cacheRetention !== "none") { - const previousPrompt = promptCache.get(sessionId); - if (previousPrompt) { - const cachedChars = commonPromptPrefixLength(previousPrompt, prompt); - cacheRead = Math.ceil(cachedChars / 4); - cacheWrite = Math.max(0, promptTokens - cacheRead); - } else { - cacheWrite = promptTokens; - } - input = 0; - promptCache.set(sessionId, prompt); - } - return { - ...message, - usage: { - input, - output: outputTokens, - cacheRead, - cacheWrite, - totalTokens: input + outputTokens + cacheRead + cacheWrite, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } - } - }; -} -function splitStringByTokenSize(text, minTokenSize, maxTokenSize) { - const chunks = []; - let index = 0; - while (index < text.length) { - const tokenSize = minTokenSize + Math.floor(Math.random() * (maxTokenSize - minTokenSize + 1)); - const charSize = Math.max(1, tokenSize * 4); - chunks.push(text.slice(index, index + charSize)); - index += charSize; - } - return chunks.length > 0 ? chunks : [""]; -} -function cloneMessage(message, api, provider, modelId) { - const cloned = structuredClone(message); - return { - ...cloned, - api, - provider, - model: modelId, - timestamp: cloned.timestamp ?? Date.now(), - usage: cloned.usage ?? DEFAULT_USAGE - }; -} -function createDeferredMessage(model, handle) { - return { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: DEFAULT_USAGE, - stopReason: "deferred", - deferred: handle, - timestamp: Date.now() - }; -} -function createErrorMessage(error, api, provider, modelId) { - return { - role: "assistant", - content: [], - api, - provider, - model: modelId, - usage: DEFAULT_USAGE, - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now() - }; -} -function createAbortedMessage(partial) { - return { - ...partial, - stopReason: "aborted", - errorMessage: "Request was aborted", - timestamp: Date.now() - }; -} -function scheduleChunk(chunk, tokensPerSecond) { - if (!tokensPerSecond || tokensPerSecond <= 0) { - return new Promise((resolve) => queueMicrotask(resolve)); - } - const delayMs = estimateTokens(chunk) / tokensPerSecond * 1e3; - return new Promise((resolve) => setTimeout(resolve, delayMs)); -} -async function streamWithDeltas(stream, message, minTokenSize, maxTokenSize, tokensPerSecond, signal) { - const partial = { ...message, content: [], stopReason: "pending" }; - if (signal?.aborted) { - const aborted = createAbortedMessage(partial); - stream.push({ type: "error", reason: "aborted", error: aborted }); - stream.end(aborted); - return; - } - stream.push({ type: "start", partial: { ...partial } }); - for (let index = 0; index < message.content.length; index++) { - if (signal?.aborted) { - const aborted = createAbortedMessage(partial); - stream.push({ type: "error", reason: "aborted", error: aborted }); - stream.end(aborted); - return; - } - const block = message.content[index]; - if (block.type === "thinking") { - partial.content = [...partial.content, { type: "thinking", thinking: "" }]; - stream.push({ type: "thinking_start", contentIndex: index, partial: { ...partial } }); - for (const chunk of splitStringByTokenSize(block.thinking, minTokenSize, maxTokenSize)) { - await scheduleChunk(chunk, tokensPerSecond); - if (signal?.aborted) { - const aborted = createAbortedMessage(partial); - stream.push({ type: "error", reason: "aborted", error: aborted }); - stream.end(aborted); - return; - } - partial.content[index].thinking += chunk; - stream.push({ type: "thinking_delta", contentIndex: index, delta: chunk, partial: { ...partial } }); - } - stream.push({ - type: "thinking_end", - contentIndex: index, - content: block.thinking, - partial: { ...partial } - }); - continue; - } - if (block.type === "text") { - partial.content = [...partial.content, { type: "text", text: "" }]; - stream.push({ type: "text_start", contentIndex: index, partial: { ...partial } }); - for (const chunk of splitStringByTokenSize(block.text, minTokenSize, maxTokenSize)) { - await scheduleChunk(chunk, tokensPerSecond); - if (signal?.aborted) { - const aborted = createAbortedMessage(partial); - stream.push({ type: "error", reason: "aborted", error: aborted }); - stream.end(aborted); - return; - } - partial.content[index].text += chunk; - stream.push({ type: "text_delta", contentIndex: index, delta: chunk, partial: { ...partial } }); - } - stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: { ...partial } }); - continue; - } - partial.content = [...partial.content, { type: "toolCall", id: block.id, name: block.name, arguments: {} }]; - stream.push({ type: "toolcall_start", contentIndex: index, partial: { ...partial } }); - const toolArguments = new ToolCallArgumentsStream(); - for (const chunk of splitStringByTokenSize(JSON.stringify(block.arguments), minTokenSize, maxTokenSize)) { - await scheduleChunk(chunk, tokensPerSecond); - if (signal?.aborted) { - const aborted = createAbortedMessage(partial); - stream.push({ type: "error", reason: "aborted", error: aborted }); - stream.end(aborted); - return; - } - partial.content[index].arguments = toolArguments.append(chunk); - stream.push({ type: "toolcall_delta", contentIndex: index, delta: chunk, partial: { ...partial } }); - } - partial.content[index].arguments = block.arguments; - stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: { ...partial } }); - } - if (message.stopReason === "pending") { - throw new Error("Faux response ended without a stop reason"); - } - if (message.stopReason === "error" || message.stopReason === "aborted") { - stream.push({ type: "error", reason: message.stopReason, error: message }); - stream.end(message); - return; - } - stream.push({ type: "done", reason: message.stopReason, message }); - stream.end(message); -} -function createFauxCore(options) { - const api = options.api ?? randomId(DEFAULT_API); - const provider = options.provider ?? DEFAULT_PROVIDER; - const minTokenSize = Math.max(1, Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE)); - const maxTokenSize = Math.max(minTokenSize, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE); - let pendingResponses = []; - const tokensPerSecond = options.tokensPerSecond; - const state = { callCount: 0, deferredFetchCount: 0, cancelledDeferred: [] }; - const promptCache = /* @__PURE__ */ new Map(); - const deferredResponses = /* @__PURE__ */ new Map(); - const modelDefinitions = options.models?.length ? options.models : [ - { - id: DEFAULT_MODEL_ID, - name: DEFAULT_MODEL_NAME, - reasoning: false, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128e3, - maxTokens: 16384 - } - ]; - const models = modelDefinitions.map((definition) => ({ - id: definition.id, - name: definition.name ?? definition.id, - api, - provider, - baseUrl: DEFAULT_BASE_URL, - reasoning: definition.reasoning ?? false, - input: definition.input ?? ["text", "image"], - inputLimits: definition.inputLimits, - cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: definition.contextWindow ?? 128e3, - maxTokens: definition.maxTokens ?? 16384 - })); - const resolveResponse = async (step, context, streamOptions, requestModel) => { - const resolved = typeof step === "function" ? await step(context, streamOptions, state, requestModel) : step; - return withUsageEstimate(cloneMessage(resolved, api, provider, requestModel.id), context, streamOptions, promptCache); - }; - const stream = (requestModel, context, streamOptions) => { - const outer = createAssistantMessageEventStream(); - const step = pendingResponses.shift(); - state.callCount++; - queueMicrotask(async () => { - try { - await streamOptions?.onResponse?.({ status: 200, headers: {} }, requestModel); - if (!step) { - let message2 = createErrorMessage(new Error("No more faux responses queued"), api, provider, requestModel.id); - message2 = withUsageEstimate(message2, context, streamOptions, promptCache); - outer.push({ type: "error", reason: "error", error: message2 }); - outer.end(message2); - return; - } - if (streamOptions?.deferred) { - const handle = { - provider: requestModel.provider, - modelId: requestModel.id, - api: requestModel.api, - id: randomId("deferred"), - ...options.deferred?.pollAfterMs !== void 0 ? { pollAfterMs: options.deferred.pollAfterMs } : {} - }; - deferredResponses.set(handle.id, { - handle, - step, - context, - options: streamOptions, - model: requestModel, - pendingFetches: Math.max(0, Math.floor(options.deferred?.pendingFetches ?? 0)), - cancelled: false - }); - await streamWithDeltas(outer, createDeferredMessage(requestModel, handle), minTokenSize, maxTokenSize, tokensPerSecond, streamOptions.signal); - return; - } - const message = await resolveResponse(step, context, streamOptions, requestModel); - await streamWithDeltas(outer, message, minTokenSize, maxTokenSize, tokensPerSecond, streamOptions?.signal); - } catch (error) { - const message = createErrorMessage(error, api, provider, requestModel.id); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - } - }); - return outer; - }; - const streamSimple = (streamModel, context, streamOptions) => stream(streamModel, context, streamOptions); - const fetchDeferred = (requestModel, handle, fetchOptions) => { - const outer = createAssistantMessageEventStream(); - state.deferredFetchCount++; - queueMicrotask(async () => { - try { - await fetchOptions?.onResponse?.({ status: 200, headers: {} }, requestModel); - const entry = deferredResponses.get(handle.id); - if (!entry || entry.handle.provider !== handle.provider || entry.handle.modelId !== handle.modelId || entry.handle.api !== handle.api) { - throw new Error(`Unknown faux deferred response: ${handle.id}`); - } - if (entry.cancelled) - throw new Error(`Faux deferred response was cancelled: ${handle.id}`); - if (entry.pendingFetches > 0) { - entry.pendingFetches--; - await streamWithDeltas(outer, createDeferredMessage(requestModel, entry.handle), minTokenSize, maxTokenSize, tokensPerSecond, fetchOptions?.signal); - return; - } - if (!entry.final) { - const { deferred: _deferred, signal: _submissionSignal, onResponse: _submissionOnResponse, ...submissionOptions } = entry.options ?? {}; - try { - entry.final = await resolveResponse(entry.step, entry.context, submissionOptions, entry.model); - } catch (error) { - entry.final = createErrorMessage(error, api, provider, entry.model.id); - } - } - await streamWithDeltas(outer, entry.final, minTokenSize, maxTokenSize, tokensPerSecond, fetchOptions?.signal); - } catch (error) { - const message = createErrorMessage(error, api, provider, requestModel.id); - outer.push({ type: "error", reason: "error", error: message }); - outer.end(message); - } - }); - return outer; - }; - const cancelDeferred = async (requestModel, handle, cancelOptions) => { - state.cancelledDeferred.push(structuredClone(handle)); - const entry = deferredResponses.get(handle.id); - if (entry) - entry.cancelled = true; - await cancelOptions?.onResponse?.({ status: 200, headers: {} }, requestModel); - }; - function getModel(requestedModelId) { - if (!requestedModelId) { - return models[0]; - } - return models.find((candidate) => candidate.id === requestedModelId); - } - return { - api, - provider, - models, - stream, - streamSimple, - fetchDeferred, - cancelDeferred, - getModel, - state, - setResponses(responses) { - pendingResponses = [...responses]; - }, - appendResponses(responses) { - pendingResponses.push(...responses); - }, - getPendingResponseCount() { - return pendingResponses.length; - } - }; -} -function fauxProvider(options = {}) { - const core = createFauxCore(options); - const provider = createProvider({ - id: core.provider, - auth: { apiKey: { name: "Faux", resolve: async () => ({ auth: {} }) } }, - models: core.models, - api: { - stream: core.stream, - streamSimple: core.streamSimple, - fetchDeferred: core.fetchDeferred, - cancelDeferred: core.cancelDeferred - } - }); - return { - provider, - api: core.api, - models: core.models, - getModel: core.getModel, - state: core.state, - setResponses: core.setResponses, - appendResponses: core.appendResponses, - getPendingResponseCount: core.getPendingResponseCount - }; -} - -// p4-test.ts -var main = `fn greeting(name: &str) -> String { format!("hello {name}") } -fn main() { - let Some(name) = std::env::args().nth(1) else { std::process::exit(7) }; - println!("{}", serde_json::json!({"greeting": greeting(&name)})); -} -#[test] -fn greeting_test() { assert_eq!(greeting("world"), "hello world", "P4_expected_failure"); } -`; -var write = (path, text) => `cat > ${path} <<'P4_FILE' -${text} -P4_FILE -`; -var phases = [ - `set -e -mkdir -p /src/demo-cli/src /src/demo-node /tmp -cd /src/demo-cli -if test -d target; then cargo clean; fi -${write("Cargo.toml", '[package]\nname="demo-cli"\nversion="0.1.0"\nedition="2024"\n[workspace]\n')} -${write("src/main.rs", main.replace('"hello world", "P4_expected_failure"', '"hello wrong", "P4_expected_failure"'))} -cargo add serde_json -cargo fmt -set +e -cargo test -- --nocapture > /tmp/p4-fail.stdout 2> /tmp/p4-fail.stderr -code=$? -set -e -cat /tmp/p4-fail.stdout /tmp/p4-fail.stderr -test "$code" = 101 -case "$(cat /tmp/p4-fail.stderr)" in *P4_expected_failure*) ;; *) exit 1 ;; esac -printf 'P4_PHASE_0_OK -'`, - `set -e -cd /src/demo-cli -${write("src/main.rs", main)} -cargo fmt -cargo test -- --nocapture -cargo build --release -test -f target/wasm32-wasip2/release/demo-cli.wasm -sh -c '/src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm world' > /tmp/p4-cli.json -case "$(cat /tmp/p4-cli.json)" in *'"greeting":"hello world"'*) ;; *) exit 1 ;; esac -set +e -sh -c '/src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm' -code=$? -set -e -test "$code" = 7 -printf 'P4_PHASE_1_OK -'`, - `set -e -${write("/src/demo-node/package.json", '{"scripts":{"test":"node test.cjs"}}')} -${write("/src/demo-node/test.cjs", 'const fs = require("node:fs");\nrequire("node:assert/strict").equal(2 + 2, 4);\nfs.writeFileSync("result.txt", "P4 npm test passed\\n");')} -id=$(box create --ttl 600) -printf '%s -' "$id" > /tmp/p4-box-id -trap 'box rm "$id"' EXIT -box put "$id" /src/demo-node /work -box exec "$id" --timeout 120 -- sh -c 'cd /work && npm test' -box get "$id" /work/result.txt /src/demo-node/result.txt -test "$(cat /src/demo-node/result.txt)" = 'P4 npm test passed' -box rm "$id" -trap - EXIT -printf 'P4_PHASE_2_OK -'`, - `set -e -undo list > /tmp/p4-undo.json -box ls > /tmp/p4-boxes -printf 'P4_PHASE_3_OK -'` -]; -function p4(host) { - const faux = fauxProvider({ provider: "openai", models: [{ id: "gpt-6.1-sol" }], tokensPerSecond: 1e6 }); - const respond = (transcript) => { - faux.appendResponses([respond]); - const messages = transcript.messages; - const input = messages.findLast((message) => message.role === "user"); - const text = (message) => typeof message.content === "string" ? message.content : (message.content ?? []).filter((part) => part.type === "text").map((part) => part.text).join("\n"); - const round = /P4 start (\d+)/.exec(text(input))?.[1]; - if (!round) throw new Error(`Unexpected P4 input: ${JSON.stringify(input)}`); - const last = messages.findLast((message) => message.role !== "system"); - let phase = 0; - if (last.role !== "user") { - const prior = /p4-\d+-(\d+)/.exec(last.toolCallId)?.[1]; - if (last.isError || prior === void 0 || !text(last).includes(`P4_PHASE_${prior}_OK`)) throw new Error(`P4 bash failed: ${JSON.stringify(last)}`); - phase = Number(prior) + 1; - } - if (phase === phases.length) return fauxAssistantMessage(`P4_COMPLETE_${round}`); - return fauxAssistantMessage([fauxToolCall("bash", { command: phases[phase], timeout: 300 }, { id: `p4-${round}-${phase}` })], { stopReason: "toolUse" }); - }; - faux.setResponses([respond]); - host.models().setProvider(faux.provider); - return { name: "p4" }; -} -export { - p4 as default -}; diff --git a/host/js/p4-test.ts b/host/js/p4-test.ts deleted file mode 100644 index e5c9d70..0000000 --- a/host/js/p4-test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** P4 fixture provider for the real console: all project work uses the installed bash tool. */ -import { fauxProvider, fauxAssistantMessage, fauxToolCall } from '@earendil-works/pi-ai/providers/faux'; - -const main = `fn greeting(name: &str) -> String { format!("hello {name}") } -fn main() { - let Some(name) = std::env::args().nth(1) else { std::process::exit(7) }; - println!("{}", serde_json::json!({"greeting": greeting(&name)})); -} -#[test] -fn greeting_test() { assert_eq!(greeting("world"), "hello world", "P4_expected_failure"); } -`; -const write = (path: string, text: string) => `cat > ${path} <<'P4_FILE'\n${text}\nP4_FILE\n`; -const phases = [ - `set -e -mkdir -p /src/demo-cli/src /src/demo-node /tmp -cd /src/demo-cli -if test -d target; then cargo clean; fi -${write('Cargo.toml', '[package]\nname="demo-cli"\nversion="0.1.0"\nedition="2024"\n[workspace]\n')} -${write('src/main.rs', main.replace('"hello world", "P4_expected_failure"', '"hello wrong", "P4_expected_failure"'))} -cargo add serde_json -cargo fmt -set +e -cargo test -- --nocapture > /tmp/p4-fail.stdout 2> /tmp/p4-fail.stderr -code=$? -set -e -cat /tmp/p4-fail.stdout /tmp/p4-fail.stderr -test "$code" = 101 -case "$(cat /tmp/p4-fail.stderr)" in *P4_expected_failure*) ;; *) exit 1 ;; esac -printf 'P4_PHASE_0_OK\n'`, - `set -e -cd /src/demo-cli -${write('src/main.rs', main)} -cargo fmt -cargo test -- --nocapture -cargo build --release -test -f target/wasm32-wasip2/release/demo-cli.wasm -sh -c '/src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm world' > /tmp/p4-cli.json -case "$(cat /tmp/p4-cli.json)" in *'"greeting":"hello world"'*) ;; *) exit 1 ;; esac -set +e -sh -c '/src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm' -code=$? -set -e -test "$code" = 7 -printf 'P4_PHASE_1_OK\n'`, - `set -e -${write('/src/demo-node/package.json', '{"scripts":{"test":"node test.cjs"}}')} -${write('/src/demo-node/test.cjs', 'const fs = require("node:fs");\nrequire("node:assert/strict").equal(2 + 2, 4);\nfs.writeFileSync("result.txt", "P4 npm test passed\\n");')} -id=$(box create --ttl 600) -printf '%s\n' "$id" > /tmp/p4-box-id -trap 'box rm "$id"' EXIT -box put "$id" /src/demo-node /work -box exec "$id" --timeout 120 -- sh -c 'cd /work && npm test' -box get "$id" /work/result.txt /src/demo-node/result.txt -test "$(cat /src/demo-node/result.txt)" = 'P4 npm test passed' -box rm "$id" -trap - EXIT -printf 'P4_PHASE_2_OK\n'`, - `set -e -undo list > /tmp/p4-undo.json -box ls > /tmp/p4-boxes -printf 'P4_PHASE_3_OK\n'`, -]; -export default function p4(host: any) { - // Replace the console's transport before resume. No real model request can occur. - const faux = fauxProvider({ provider: 'openai', models: [{ id: 'gpt-6.1-sol' }], tokensPerSecond: 1000000 }); - const respond = (transcript: any): any => { - faux.appendResponses([respond]); - const messages = transcript.messages; - const input = messages.findLast((message: any) => message.role === 'user'); - const text = (message: any) => typeof message.content === 'string' ? message.content : (message.content ?? []).filter((part: any) => part.type === 'text').map((part: any) => part.text).join('\n'); - const round = /P4 start (\d+)/.exec(text(input))?.[1]; - if (!round) throw new Error(`Unexpected P4 input: ${JSON.stringify(input)}`); - const last = messages.findLast((message: any) => message.role !== 'system'); - let phase = 0; - if (last.role !== 'user') { - const prior = /p4-\d+-(\d+)/.exec(last.toolCallId)?.[1]; - if (last.isError || prior === undefined || !text(last).includes(`P4_PHASE_${prior}_OK`)) throw new Error(`P4 bash failed: ${JSON.stringify(last)}`); - phase = Number(prior) + 1; - } - if (phase === phases.length) return fauxAssistantMessage(`P4_COMPLETE_${round}`); - return fauxAssistantMessage([fauxToolCall('bash', { command: phases[phase], timeout: 300 }, { id: `p4-${round}-${phase}` })], { stopReason: 'toolUse' }); - }; - faux.setResponses([respond]); - host.models().setProvider(faux.provider); - return { name: 'p4' }; -} diff --git a/host/js/package.json b/host/js/package.json index baf5efb..8cefdfc 100644 --- a/host/js/package.json +++ b/host/js/package.json @@ -2,7 +2,7 @@ "private": true, "type": "module", "scripts": { - "bundle": "export NODE_PATH=\"$PWD/node_modules\"; nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild p3-test.ts --bundle --format=esm --platform=browser --outfile=p3-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js && esbuild watchdog-test.ts --bundle --format=esm --platform=browser --outfile=watchdog-test-bundle.js && esbuild workspaces-test.ts --bundle --format=esm --platform=browser --outfile=workspaces-test-bundle.js && esbuild goal-questions-test.ts --bundle --format=esm --platform=browser --outfile=goal-questions-test-bundle.js && esbuild dream-test.ts --bundle --format=esm --platform=browser --outfile=dream-test-bundle.js && esbuild p4-test.ts --bundle --format=esm --platform=browser --outfile=p4-test-bundle.js && esbuild harness-api-test.ts --bundle --format=esm --platform=browser --outfile=harness-api-test-bundle.js && esbuild provider-models-test.ts --bundle --format=esm --platform=browser --outfile=provider-models-test-bundle.js" + "bundle": "export NODE_PATH=\"$PWD/node_modules\"; nu apply-patch.nu && esbuild main.ts --bundle --format=esm --platform=browser --outfile=bundle.js && esbuild test.ts --bundle --format=esm --platform=browser --outfile=test-bundle.js && esbuild durable-test.ts --bundle --format=esm --platform=browser --outfile=durable-test-bundle.js && esbuild foundation-test.ts --bundle --format=esm --platform=browser --outfile=foundation-test-bundle.js && esbuild extensions-test.ts --bundle --format=esm --platform=browser --outfile=extensions-test-bundle.js && esbuild events-test.ts --bundle --format=esm --platform=browser --outfile=events-test-bundle.js && esbuild subagents-test.ts --bundle --format=esm --platform=browser --outfile=subagents-test-bundle.js && esbuild core-ext-test.ts --bundle --format=esm --platform=browser --outfile=core-ext-test-bundle.js && esbuild plan-todos-test.ts --bundle --format=esm --platform=browser --outfile=plan-todos-test-bundle.js && esbuild p2-test.ts --bundle --format=esm --platform=browser --outfile=p2-test-bundle.js && esbuild chain-test.ts --bundle --format=esm --platform=browser --outfile=chain-test-bundle.js && esbuild undo-harness-test.ts --bundle --format=esm --platform=browser --outfile=undo-harness-test-bundle.js && esbuild messages-test.ts --bundle --format=esm --platform=browser --outfile=messages-test-bundle.js && esbuild p3-test.ts --bundle --format=esm --platform=browser --outfile=p3-test-bundle.js && esbuild automations-test.ts --bundle --format=esm --platform=browser --outfile=automations-test-bundle.js && esbuild watchdog-test.ts --bundle --format=esm --platform=browser --outfile=watchdog-test-bundle.js && esbuild workspaces-test.ts --bundle --format=esm --platform=browser --outfile=workspaces-test-bundle.js && esbuild goal-questions-test.ts --bundle --format=esm --platform=browser --outfile=goal-questions-test-bundle.js && esbuild dream-test.ts --bundle --format=esm --platform=browser --outfile=dream-test-bundle.js && esbuild harness-api-test.ts --bundle --format=esm --platform=browser --outfile=harness-api-test-bundle.js && esbuild provider-models-test.ts --bundle --format=esm --platform=browser --outfile=provider-models-test-bundle.js" }, "dependencies": { "@earendil-works/chord": "1.5.0", diff --git a/host/tests/it/main.rs b/host/tests/it/main.rs index 7e65cf2..0750893 100644 --- a/host/tests/it/main.rs +++ b/host/tests/it/main.rs @@ -53,7 +53,6 @@ mod net; mod p1; mod p2; mod p3; -mod p4; mod plan_todos; mod process; mod provider_models; diff --git a/host/tests/it/p4.rs b/host/tests/it/p4.rs deleted file mode 100644 index 3628085..0000000 --- a/host/tests/it/p4.rs +++ /dev/null @@ -1,293 +0,0 @@ -//! P4 exit: the real console agent builds Rust and Node projects, then inspects undo history. -use anyhow::{Context, ensure}; -use sausage::world::World; -use sausage_vfs::journal::Entry; -use std::{ - fs, - io::Write, - path::{Path, PathBuf}, - process::{Child, Command, Stdio}, - time::{Duration, Instant}, -}; - -fn checked(command: &mut Command) -> anyhow::Result<()> { - ensure!(command.status()?.success(), "command failed: {command:?}"); - Ok(()) -} -fn prepare(dir: &Path) -> anyhow::Result { - let source = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); - let userland = source.join("userland"); - let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR")?); - let bundle = Path::new(env!("CARGO_TARGET_TMPDIR")).join("p4-bundle"); - let revision = Command::new("git") - .current_dir(&source) - .args(["rev-parse", "HEAD"]) - .output()?; - ensure!(revision.status.success(), "fixture revision"); - if fs::read(bundle.join("revision")).ok().as_deref() != Some(revision.stdout.as_slice()) - || !bundle.join("bin/tar.wasm").is_file() - { - if !userland.join("cwd-init/init.o").exists() { - crate::common::cwd_init(); - } - checked(Command::new("cargo").current_dir(&userland).args([ - "build", - "--release", - "--locked", - "-p", - "git", - "-p", - "world-cargo", - "-p", - "box-cli", - "-p", - "undo", - "-p", - "sausage-coreutils", - "-p", - "sausage-archive-tools", - ]))?; - ensure!( - crate::common::build_brush()?.success(), - "shell build failed" - ); - fs::create_dir_all(bundle.join("bin"))?; - fs::create_dir_all(bundle.join("src"))?; - for (binary, name) in [ - ("brush", "sh"), - ("brush", "bash"), - ("git", "git"), - ("tar", "tar"), - ("cargo", "cargo"), - ("box", "box"), - ("undo", "undo"), - ("cat", "cat"), - ("mkdir", "mkdir"), - ] { - fs::copy( - target.join(format!("wasm32-wasip2/release/{binary}.wasm")), - bundle.join(format!("bin/{name}.wasm")), - )?; - } - fs::write(bundle.join("revision"), revision.stdout)?; - } - World::open_with_userland(dir, Some(&bundle))?; - let repo = dir.join("world/src/sausage"); - for extension in ["core", "models", "subagents"] { - crate::common::copy_tree( - &source.join("extensions").join(extension), - &repo.join("extensions").join(extension), - )?; - } - crate::common::write_config( - dir, - "console", - serde_json::json!([ - {"name":"console","description":"P4 console","model":"openai/gpt-6.1-sol","prompt":"Build the requested programs","tools":["bash"]} - ]), - )?; - fs::create_dir_all(repo.join("extensions/p4"))?; - fs::write( - repo.join("extensions/p4/server.ts"), - include_str!("../../js/p4-test-bundle.js"), - )?; - for args in [ - vec!["init", "-b", "main"], - vec!["add", "."], - vec!["commit", "--no-gpg-sign", "-m", "P4 fixture"], - ] { - checked( - Command::new("git") - .current_dir(&repo) - .args(args) - .env("GIT_AUTHOR_NAME", "P4 test") - .env("GIT_AUTHOR_EMAIL", "p4@example.com") - .env("GIT_COMMITTER_NAME", "P4 test") - .env("GIT_COMMITTER_EMAIL", "p4@example.com"), - )?; - } - Ok(bundle) -} -struct Running(Child); -impl Drop for Running { - fn drop(&mut self) { - let _ = self.0.kill(); - let _ = self.0.wait(); - } -} -fn round(dir: &Path, number: usize) -> anyhow::Result<()> { - let stdout = dir.join(format!("round-{number}.stdout")); - let stderr = dir.join(format!("round-{number}.stderr")); - let mut child = Running( - Command::new(env!("CARGO_BIN_EXE_sausage")) - .arg("run") - .arg(dir) - .env_remove("CREDENTIALS_DIRECTORY") - .stdin(Stdio::piped()) - .stdout(fs::File::create(&stdout)?) - .stderr(fs::File::create(&stderr)?) - .spawn()?, - ); - writeln!( - child.0.stdin.take().context("console stdin")?, - "P4 start {number}" - )?; - let deadline = Instant::now() + Duration::from_secs(600); - loop { - if fs::read_to_string(&stdout)?.contains(&format!("P4_COMPLETE_{number}")) { - break; - } - if let Some(status) = child.0.try_wait()? { - anyhow::bail!("console exited: {status}"); - } - ensure!( - Instant::now() < deadline, - "P4 round {number} exceeded its deadline" - ); - std::thread::yield_now(); - } - checked(Command::new("kill").args(["-TERM", &child.0.id().to_string()]))?; - let deadline = Instant::now() + Duration::from_secs(20); - loop { - if let Some(status) = child.0.try_wait()? { - ensure!(status.success(), "console shutdown: {status}"); - break; - } - ensure!( - Instant::now() < deadline, - "console shutdown exceeded its deadline" - ); - std::thread::yield_now(); - } - Ok(()) -} -fn boxes_removed(dir: &Path, bundle: &Path) -> anyhow::Result<()> { - let path = dir.join("world/tmp/p4-box-id"); - if !path.exists() { - return Ok(()); - } - let id = fs::read_to_string(path)?; - let id = id.trim(); - ensure!( - id.starts_with("tc-") && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'), - "invalid box ID" - ); - // The agent's EXIT trap is primary cleanup. Recover its box even if the console fails. - let output = Command::new(env!("CARGO_BIN_EXE_sausage")) - .arg("sh") - .arg(dir) - .arg("--userland") - .arg(bundle) - .arg("-c") - .arg("box ls") - .env_remove("CREDENTIALS_DIRECTORY") - .output()?; - ensure!( - output.status.success(), - "box ls: {}", - String::from_utf8_lossy(&output.stderr) - ); - if String::from_utf8_lossy(&output.stdout).contains(id) { - checked( - Command::new(env!("CARGO_BIN_EXE_sausage")) - .arg("sh") - .arg(dir) - .arg("--userland") - .arg(bundle) - .arg("-c") - .arg(format!("box rm {id}")) - .env_remove("CREDENTIALS_DIRECTORY"), - )?; - anyhow::bail!("Agent left {id} active. Test cleanup removed it"); - } - println!("P4 box absent from service inventory: {id}"); - Ok(()) -} -fn cache_hits(dir: &Path) -> anyhow::Result { - let workspace = fs::read_dir(dir.join("host/builds"))? - .next() - .context("builder workspace")?? - .path(); - let output = Command::new("kache") - .arg("stats") - .env("KACHE_CACHE_DIR", workspace.join("kache")) - .output()?; - ensure!( - output.status.success(), - "kache: {}", - String::from_utf8_lossy(&output.stderr) - ); - let text = String::from_utf8(output.stdout)?; - println!("P4 PRIVATE CACHE:\n{text}"); - let words: Vec<_> = text.split_whitespace().collect(); - words - .windows(2) - .find_map(|pair| { - if pair[1] == "of" { - pair[0].replace(',', "").parse().ok() - } else { - None - } - }) - .context("cache hit count") -} -#[test] -fn agent_builds_rust_and_node_twice() -> anyhow::Result<()> { - let dir = tempfile::tempdir()?; - let bundle = prepare(dir.path())?; - let mut first_hits = 0; - for number in 1..=2 { - let outcome = round(dir.path(), number); - let cleanup = boxes_removed(dir.path(), &bundle); - if outcome.is_err() { - for stream in ["stdout", "stderr"] { - eprintln!( - "P4 round {number} {stream}:\n{}", - fs::read_to_string(dir.path().join(format!("round-{number}.{stream}")))? - ); - } - } - outcome?; - cleanup?; - let world = dir.path().join("world"); - ensure!( - world - .join("src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm") - .is_file(), - "missing world artifact" - ); - ensure!( - fs::read_to_string(world.join("src/demo-cli/Cargo.toml"))?.contains("serde_json"), - "cargo add did not reach world" - ); - ensure!( - fs::read_to_string(world.join("src/demo-node/result.txt"))? == "P4 npm test passed\n", - "Node result missing" - ); - let entries: Vec = - serde_json::from_slice(&fs::read(world.join("tmp/p4-undo.json"))?)?; - for (phase, path) in [ - (0, "/src/demo-cli/Cargo.toml"), - (1, "/src/demo-cli/src/main.rs"), - ] { - let tag = format!("p4-{number}-{phase}"); - ensure!( - entries.iter().any(|entry| entry.tag == tag - && entry.program == "bash" - && entry.changed_paths.iter().any(|p| p == path)), - "undo lost {tag}: {path}" - ); - } - let hits = cache_hits(dir.path())?; - if number == 1 { - first_hits = hits; - } else { - ensure!( - hits > first_hits, - "second run missed kache: {first_hits} -> {hits}" - ); - println!("P4 CACHE SECOND RUN: {first_hits} -> {hits} hits"); - } - } - Ok(()) -}