Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,33 @@ node ./bin/aas.mjs demo --domain inventory --fault duplicate --prove rail

Both domains keep their own policy fixture and their own remedy scope field
(`max_amount_minor` for refunds, `max_quantity` for allocations), so neither
is disguised as the other. Everything remains synthetic: no warehouse,
is disguised as the other. The GUI exposes the same choice with a Domain
selector. Everything remains synthetic: no warehouse,
merchant, payment, or external provider integration is involved, and a
recorded review proves the handoff rather than any real-world reversibility.

## Guided case workbench

The local GUI includes four explicit synthetic scenario presets: clean settlement,
policy refusal, duplicate compensation with same-case review, and settled-action
review. Apply a preset, select refund or inventory, then press **Run stack**.
Applying a preset never starts work. Domain identity is persisted in new reports;
older bundles display an unknown domain instead of guessing.

Load recent case history to search run identity, policy, domain, or review metadata
and filter settled or compensated outcomes. Filters affect the displayed list;
comparison selectors retain all loaded cases. History is bounded and may omit older
or unreadable cases. **Inspect left case** loads a saved report and its bindings in
a separate panel and enables an identity-checked download. Inspection does not
verify source truth. Use imported replay to re-verify synthetic evidence.

GUI runs, Python discovery, and replay verification execute in a worker thread,
so synchronous component commands leave HTTP health checks and admission
responsive. Existing child timeouts and output caps remain enforced. The server
retains admission until the worker exits, including after a client disconnect.
Only one run or replay upload is admitted at a time per GUI server. Busy callers
receive HTTP 503 with Retry-After; retry after the current operation finishes.
Ambiguous duplicate run or comparison options are rejected. File changes clear
stale replay results, and oversized imports are rejected before browser file reads
as well as at the server boundary. No preset, inspection, or history workflow
performs real account operations.
46 changes: 46 additions & 0 deletions bin/aas-gui-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
import { runDemo, replayBundle, selectPython } from "./aas.mjs";

const GUI_TASK_KIND = "agent-action-stack.gui-task/v1";

/** Keep synchronous component processes off the HTTP event loop.
* Each task owns one worker through its exit, so a server lease never ends
* while component work is still running. Existing child bounds stay intact.
*/
export function runGuiTask(task) {
return new Promise((resolve, reject) => {
let result;
let failure;
const worker = new Worker(new URL(import.meta.url), { workerData: { kind: GUI_TASK_KIND, task } });
worker.on("message", (message) => { result = message; });
worker.on("error", (error) => { failure = error; });
worker.on("exit", (code) => {
if (failure) reject(failure);
else if (code !== 0) reject(new Error(`GUI worker exited with code ${code}.`));
else if (result?.ok === true) resolve(result.value);
else reject(new Error(result?.error ?? "GUI worker exited without a result."));
});
});
}

// Importing the GUI inside another worker must not claim its parent channel.
if (!isMainThread && workerData?.kind === GUI_TASK_KIND) {
try {
const task = workerData.task;
let value;
if (task?.operation === "run") {
const options = task.options ?? {};
const python = options.python ?? selectPython();
value = await runDemo(task.args, { ...options, python });
} else if (task?.operation === "replay") {
value = replayBundle(task.bundle, task.options ?? {});
} else {
throw new Error("Unsupported GUI worker operation.");
}
parentPort.postMessage({ ok: true, value });
} catch (error) {
parentPort.postMessage({ ok: false, error: error.message });
} finally {
parentPort.close();
}
}
Loading
Loading