@agdf/mcp-server 0.0.0-stage → 0.20.0
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/LICENSE +201 -0
- package/NOTICE +10 -0
- package/README.md +135 -2
- package/bin/agdf-mcp.js +24 -0
- package/package.json +41 -4
- package/src/cockpit-resource.js +20 -0
- package/src/generation-entry.js +86 -0
- package/src/generation-protocol.js +44 -0
- package/src/generation.js +211 -0
- package/src/main.js +56 -0
- package/src/server.js +187 -0
- package/src/ui-capability.js +17 -0
- package/src/worker-entry.js +39 -0
- package/src/worker.js +134 -0
- package/ui/cockpit.html +109 -0
- package/ui/decision-manifest.json +8 -0
- package/ui/decision.html +108 -0
- package/ui/manifest.json +8 -0
package/src/worker.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
|
|
3
|
+
const WORKER_URL = new URL("./worker-entry.js", import.meta.url);
|
|
4
|
+
const PHASES = new Set(['runtime_provenance', 'operation', 'result']);
|
|
5
|
+
|
|
6
|
+
export class DispatchExecutionError extends Error {
|
|
7
|
+
constructor(code, dispatch = 'unknown', field, { cause_code, worker_phase } = {}) {
|
|
8
|
+
super(code);
|
|
9
|
+
this.name = "DispatchExecutionError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.dispatch = dispatch;
|
|
12
|
+
this.facts = { code, dispatch, ...(field ? { field } : {}),
|
|
13
|
+
...(cause_code ? { cause_code } : {}), ...(worker_phase ? { worker_phase } : {}) };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Forwards Core lock ownership announcements from a dispatch thread to its executor.
|
|
18
|
+
// Used by worker-entry.js; posting is synchronous, and Node drains posted messages
|
|
19
|
+
// before it emits the worker's exit event.
|
|
20
|
+
export function ownedLockForwarder(send) {
|
|
21
|
+
return ({ phase, lock_path: lockPath, token }) => send({ type: 'lock', phase, lock_path: lockPath, token });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createWorkerDispatchExecutor({
|
|
25
|
+
surface, expectedVersion, commandGroups, timeoutMs = 10_000, WorkerClass = Worker,
|
|
26
|
+
onEvent = () => {}, releaseOwnedLock = null,
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 10_000) throw Error('dispatch_deadline_invalid');
|
|
29
|
+
// The request budget includes both execution and confirmed worker shutdown.
|
|
30
|
+
// Reserve up to one second instead of beginning shutdown at the deadline.
|
|
31
|
+
const terminationReserveMs = Math.min(1_000, Math.max(1, Math.floor(timeoutMs / 4)));
|
|
32
|
+
let active = null;
|
|
33
|
+
const event = (phase, started) => {
|
|
34
|
+
try { onEvent({ phase, elapsed_ms: performance.now() - started }); } catch {}
|
|
35
|
+
};
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
async execute(argumentsValue, { signal, toolName } = {}) {
|
|
38
|
+
if (active) throw new DispatchExecutionError("dispatch_busy", 'not_dispatched');
|
|
39
|
+
if (signal?.aborted) throw new DispatchExecutionError("dispatch_cancelled", 'not_dispatched');
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let settled = false, exited = false, completion, workerPhase = 'worker_start';
|
|
42
|
+
let confirmExit;
|
|
43
|
+
const actualExit = new Promise(resolve => { confirmExit = resolve; });
|
|
44
|
+
const started = performance.now(), deadline = started + timeoutMs;
|
|
45
|
+
let worker;
|
|
46
|
+
try { worker = new WorkerClass(WORKER_URL, {
|
|
47
|
+
workerData: { surface, expectedVersion, commandGroups, argumentsValue, toolName: toolName ?? null },
|
|
48
|
+
}); } catch { reject(new DispatchExecutionError('dispatch_worker_failed', 'not_dispatched')); return; }
|
|
49
|
+
event('worker_start', started);
|
|
50
|
+
const current = { worker, timer: null, cleanupSignal: null, cancel: () => finish(reject, new DispatchExecutionError("dispatch_cancelled")) };
|
|
51
|
+
active = current;
|
|
52
|
+
// Locks this worker announced and has not reported released. Its thread shares the
|
|
53
|
+
// server PID, so stale-lock recovery would treat them as live forever.
|
|
54
|
+
const held = new Map();
|
|
55
|
+
const releaseHeldLocks = () => {
|
|
56
|
+
if (!held.size) return;
|
|
57
|
+
let released = 0;
|
|
58
|
+
for (const [token, lockPath] of held) {
|
|
59
|
+
try { if (releaseOwnedLock?.(lockPath, token) === 'released') released++; } catch {}
|
|
60
|
+
}
|
|
61
|
+
held.clear();
|
|
62
|
+
try { onEvent({ phase: 'locks_released', elapsed_ms: performance.now() - started, count: released }); } catch {}
|
|
63
|
+
};
|
|
64
|
+
// Called only after actual exit or confirmed termination: the thread no longer runs.
|
|
65
|
+
const release = () => { releaseHeldLocks(); if (active === current) active = null; };
|
|
66
|
+
const finish = (callback, value) => {
|
|
67
|
+
if (settled) return completion;
|
|
68
|
+
settled = true;
|
|
69
|
+
clearTimeout(current.timer);
|
|
70
|
+
current.cleanupSignal?.();
|
|
71
|
+
event('termination', started);
|
|
72
|
+
completion = (async () => {
|
|
73
|
+
let confirmationTimer;
|
|
74
|
+
// Confirmation consumes the remaining request budget; it cannot add another full deadline.
|
|
75
|
+
const bounded = new Promise(done => {
|
|
76
|
+
confirmationTimer = setTimeout(() => done(false), Math.max(0, deadline - performance.now()));
|
|
77
|
+
});
|
|
78
|
+
let ending;
|
|
79
|
+
try { ending = exited ? actualExit : Promise.resolve(worker.terminate()).then(() => {
|
|
80
|
+
exited = true; release(); return true;
|
|
81
|
+
}, () => actualExit); }
|
|
82
|
+
catch { ending = actualExit; }
|
|
83
|
+
const confirmed = exited || await Promise.race([ending, actualExit, bounded]);
|
|
84
|
+
clearTimeout(confirmationTimer);
|
|
85
|
+
if (confirmed) {
|
|
86
|
+
release(); event('terminated', started); callback(value);
|
|
87
|
+
} else {
|
|
88
|
+
// A late actual exit may release capacity; rejection alone never proves termination.
|
|
89
|
+
event('termination_unconfirmed', started);
|
|
90
|
+
reject(new DispatchExecutionError('dispatch_worker_termination_unconfirmed', 'unknown', undefined, {
|
|
91
|
+
...(callback === reject && value instanceof DispatchExecutionError ? { cause_code: value.code } : {}),
|
|
92
|
+
worker_phase: workerPhase,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
return completion;
|
|
97
|
+
};
|
|
98
|
+
const cancel = () => finish(reject, new DispatchExecutionError("dispatch_cancelled"));
|
|
99
|
+
if (signal) {
|
|
100
|
+
current.cleanupSignal = () => signal.removeEventListener("abort", cancel);
|
|
101
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
102
|
+
}
|
|
103
|
+
current.timer = setTimeout(() => finish(reject, new DispatchExecutionError("dispatch_timeout")), timeoutMs - terminationReserveMs);
|
|
104
|
+
worker.on("message", message => {
|
|
105
|
+
// Lock messages also arrive after settlement while the worker drains.
|
|
106
|
+
if (message?.type === 'lock') {
|
|
107
|
+
if (typeof message.token === 'string' && typeof message.lock_path === 'string') {
|
|
108
|
+
if (message.phase === 'intent') held.set(message.token, message.lock_path);
|
|
109
|
+
else if (message.phase === 'released') held.delete(message.token);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (settled) return;
|
|
114
|
+
if (message?.type === 'phase') {
|
|
115
|
+
if (PHASES.has(message.phase)) { workerPhase = message.phase; event(message.phase, started); }
|
|
116
|
+
} else if (message?.type === "result") { workerPhase = 'result'; finish(resolve, message.result); }
|
|
117
|
+
else if (message?.type === 'failure') finish(reject, new DispatchExecutionError(message.code, message.dispatch, message.field));
|
|
118
|
+
else finish(reject, new DispatchExecutionError('dispatch_worker_failed'));
|
|
119
|
+
});
|
|
120
|
+
worker.once("error", () => finish(reject, new DispatchExecutionError("dispatch_worker_failed")));
|
|
121
|
+
worker.once("exit", () => {
|
|
122
|
+
exited = true;
|
|
123
|
+
confirmExit(true);
|
|
124
|
+
if (settled) release();
|
|
125
|
+
else finish(reject, new DispatchExecutionError("dispatch_worker_failed"));
|
|
126
|
+
});
|
|
127
|
+
// Cover an abort racing listener installation.
|
|
128
|
+
if (signal?.aborted) cancel();
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
async close() { await active?.cancel(); },
|
|
132
|
+
get active() { return Boolean(active); },
|
|
133
|
+
});
|
|
134
|
+
}
|