@agdf/mcp-server 0.20.2 → 0.31.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/README.md +143 -7
- package/package.json +8 -3
- package/src/generation-entry.js +3 -85
- package/src/generation-protocol.js +2 -44
- package/src/generation-runtime.js +103 -0
- package/src/generation-thread-entry.js +8 -0
- package/src/generation.js +64 -20
- package/src/main.js +43 -10
- package/src/next-action.js +227 -0
- package/src/prepared-call-transport.js +306 -0
- package/src/safe-read-continuation.js +62 -0
- package/src/server.js +159 -46
- package/src/stdio-output.js +42 -0
- package/src/worker-entry.js +2 -2
- package/src/worker.js +53 -7
- package/ui/cockpit.html +31 -31
- package/ui/decision-manifest.json +3 -3
- package/ui/decision.html +36 -36
- package/ui/manifest.json +4 -4
package/src/generation.js
CHANGED
|
@@ -8,7 +8,7 @@ import { DispatchExecutionError } from './worker.js';
|
|
|
8
8
|
// One live facade, three directly owned children, no process registry or PID
|
|
9
9
|
// discovery. Capacity includes startup and unconfirmed cleanup, not just current.
|
|
10
10
|
export async function createOwnedGenerationFacade({ runtime, publisher, createControlRuntime, surface, cockpitDir, cockpitMode,
|
|
11
|
-
Fork = fork, qualifyMs = L.qualify, closeMs = L.close, invokeMs = L.qualify, onEvent = () => {} } = {}) {
|
|
11
|
+
generationTransport, Fork = fork, qualifyMs = L.qualify, closeMs = L.close, invokeMs = L.qualify, onEvent = () => {} } = {}) {
|
|
12
12
|
if (![qualifyMs, closeMs, invokeMs].every(value => Number.isSafeInteger(value) && value > 0)
|
|
13
13
|
|| qualifyMs > L.qualify || closeMs > L.close || invokeMs > L.qualify) throw Error('generation_deadline_invalid');
|
|
14
14
|
const epoch = randomUUID(), slots = new Map(), routes = new Map(), presentations = new Map(), operations = new Map(), receipts = new Map();
|
|
@@ -64,10 +64,16 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
64
64
|
unknown: new Set(), inflight: 0, exited: false, closeAck: false, observed: null };
|
|
65
65
|
slots.set(row.id, row); event(row, 'reserved'); // Reservation precedes acquisition/load/spawn.
|
|
66
66
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
if (generationTransport) {
|
|
68
|
+
// This is a logical private connection. Actual Worker and claim ownership
|
|
69
|
+
// stay in the detached owner; no ChildProcess/Worker impersonation.
|
|
70
|
+
row.child = await generationTransport.open({ packageInfo, epoch, generation: row.id });
|
|
71
|
+
} else {
|
|
72
|
+
const claim = publisher.acquire(packageInfo, { ownerEpoch: epoch, generation: row.id });
|
|
73
|
+
row.child = Fork(join(packageInfo.packageRoot, 'src/generation-entry.js'), [epoch, String(row.id)],
|
|
74
|
+
{ stdio: ['ignore', 'ignore', 'ignore', 'ipc'], serialization: 'json', execArgv: [] });
|
|
75
|
+
claim.bindOwnedChild(row.child, { confirmedEnd: () => row.closeAck && !row.inflight && !row.unknown.size });
|
|
76
|
+
}
|
|
71
77
|
row.child.on('message', reply => {
|
|
72
78
|
if (!validGenerationMessage(reply, { epoch, generation: row.id, reply: true })) { event(row, 'invalid_reply'); return; }
|
|
73
79
|
const pending = row.pending.get(reply.request_id);
|
|
@@ -81,13 +87,18 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
81
87
|
pending.resolve(reply);
|
|
82
88
|
}
|
|
83
89
|
});
|
|
84
|
-
row.child.once('exit', () => {
|
|
90
|
+
row.child.once('exit', (code, signal) => {
|
|
85
91
|
row.exited = true;
|
|
86
92
|
for (const pending of row.pending.values()) { clearTimeout(pending.timer); pending.reject(new DispatchExecutionError('generation_connection_lost')); }
|
|
87
93
|
row.pending.clear();
|
|
88
|
-
if (row.closeAck && !row.inflight && !row.unknown.size) { slots.delete(row.id); row.state = 'closed'; event(row, 'confirmed_exit'); row.end?.(true); }
|
|
94
|
+
if (code === 0 && !signal && row.closeAck && !row.inflight && !row.unknown.size) { slots.delete(row.id); row.state = 'closed'; event(row, 'confirmed_exit'); row.end?.(true); }
|
|
89
95
|
else { row.state = 'close_unconfirmed'; event(row, 'unexpected_exit'); row.end?.(false); }
|
|
90
96
|
});
|
|
97
|
+
row.child.once('disconnect', () => {
|
|
98
|
+
row.state = 'close_unconfirmed'; event(row, 'connection_lost');
|
|
99
|
+
for (const pending of row.pending.values()) { clearTimeout(pending.timer); pending.reject(new DispatchExecutionError('generation_connection_lost')); }
|
|
100
|
+
row.pending.clear();
|
|
101
|
+
});
|
|
91
102
|
row.child.once('error', () => { row.state = 'close_unconfirmed'; event(row, 'spawn_failed'); });
|
|
92
103
|
row.state = 'qualifying'; event(row, 'qualifying');
|
|
93
104
|
const qualified = (await request(row, 'qualify', { surface, commandGroups: [...(runtime.trustedContext.commandGroups ?? [])],
|
|
@@ -105,6 +116,11 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
105
116
|
}
|
|
106
117
|
const control = {
|
|
107
118
|
status,
|
|
119
|
+
originalInput(input) {
|
|
120
|
+
const prior = receipts.get(input.operation_id);
|
|
121
|
+
if (!prior) return null;
|
|
122
|
+
return { known: true, resolution: prior.resolution ?? null };
|
|
123
|
+
},
|
|
108
124
|
async refresh() { await Promise.all([...slots.values()].filter(row => !row.exited).map(observe)); return status(); },
|
|
109
125
|
inspect(id, input) { const record = receipts.get(id);
|
|
110
126
|
if (record && (record.input.run_id !== input.run_id || record.input.primary_target !== input.primary_target
|
|
@@ -112,18 +128,23 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
112
128
|
return record ? { status: record.result ? 'recorded' : 'pending',
|
|
113
129
|
request_digest: record.digest, result: record.result, current: status(), authorizes: false }
|
|
114
130
|
: { status: 'unknown', reason: 'connection_control_record_unavailable', current: status(), authorizes: false }; },
|
|
115
|
-
async replace({ input, candidate, requestDigest, revalidate }) {
|
|
131
|
+
async replace({ input, candidate, requestDigest, resolution, revalidate }) {
|
|
116
132
|
const prior = receipts.get(input.operation_id);
|
|
117
133
|
if (prior) return prior.digest !== requestDigest ? failure('operation_payload_conflict')
|
|
118
134
|
: prior.result ? { ...prior.result, outcome: prior.result.outcome === 'accepted' ? 'already_applied' : prior.result.outcome, historical_effect: true, current: status() }
|
|
119
135
|
: { outcome: 'effect_unknown', reason: 'generation_replacement_pending', current: status(), authorizes: false };
|
|
120
136
|
if (receipts.size >= L.receipts) return failure('connection_control_capacity');
|
|
121
137
|
if (closed || replacing) return failure(closed ? 'connection_closed' : 'generation_replacement_busy');
|
|
122
|
-
const record = { digest: requestDigest, input, result: null }; receipts.set(input.operation_id, record); replacing = true;
|
|
138
|
+
const record = { digest: requestDigest, input, resolution, result: null }; receipts.set(input.operation_id, record); replacing = true;
|
|
123
139
|
let candidateRow;
|
|
124
140
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
const recheck = () => {
|
|
142
|
+
if (closed) throw new DispatchExecutionError('connection_closed', 'not_dispatched');
|
|
143
|
+
controlRuntimeInstance?.verifyProducer();
|
|
144
|
+
revalidate();
|
|
145
|
+
};
|
|
146
|
+
recheck(); const packageInfo = publisher.resolve(candidate);
|
|
147
|
+
candidateRow = await reserve(packageInfo, actualContext); recheck();
|
|
127
148
|
const previous = current; current = candidateRow; current.state = 'current';
|
|
128
149
|
if (previous) previous.state = previous.exited ? 'close_unconfirmed' : 'draining';
|
|
129
150
|
event(current, 'admitted'); record.result = { outcome: 'accepted', reason: null, operation_id: input.operation_id,
|
|
@@ -135,44 +156,66 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
135
156
|
record.result.current = status(); return record.result;
|
|
136
157
|
},
|
|
137
158
|
};
|
|
138
|
-
|
|
159
|
+
// Ordinary requests belong to the fully qualified generation. Only the rare
|
|
160
|
+
// facade control branches need another Core instance; share one initialization.
|
|
161
|
+
let controlRuntimePromise, controlRuntimeInstance;
|
|
162
|
+
const getControlRuntime = () => controlRuntimePromise ??= Promise.resolve().then(() => createControlRuntime(control)).then(async created => {
|
|
163
|
+
const producer = created?.producerIdentity;
|
|
164
|
+
const code = created?.trustedContext?.expectedVersion !== runtime.trustedContext.expectedVersion ? 'runtime_version_mismatch'
|
|
165
|
+
: created?.trustedContext?.provenanceStatus !== 'matched'
|
|
166
|
+
|| !producer || producer.server_digest !== publisher.initial.digest || producer.dispatcher_digest !== publisher.initial.dispatcherDigest
|
|
167
|
+
|| producer.sdk_digest !== publisher.initial.sdkDigest || typeof created.verifyProducer !== 'function'
|
|
168
|
+
? 'runtime_provenance_invalid' : null;
|
|
169
|
+
if (code) {
|
|
170
|
+
await created?.close?.(); throw new DispatchExecutionError(code, 'not_dispatched');
|
|
171
|
+
}
|
|
172
|
+
controlRuntimeInstance = created;
|
|
173
|
+
return created;
|
|
174
|
+
}).catch(error => { throw new DispatchExecutionError(generationFailureReason(error), 'not_dispatched'); });
|
|
139
175
|
function operationKey(input, id = input.operation_id) { return JSON.stringify([input.primary_target, input.run_id, id]); }
|
|
140
176
|
async function invoke(name, args, host = {}, signal) {
|
|
141
177
|
if (signal?.aborted) throw new DispatchExecutionError('dispatch_cancelled', 'not_dispatched');
|
|
142
178
|
if (closed) throw new DispatchExecutionError('connection_closed', 'not_dispatched');
|
|
143
179
|
actualContext = { client: host.client, uiSupported: host.uiSupported === true };
|
|
180
|
+
const canonical = Boolean(args.operation_id) && !runtime.isEphemeralControlCommand?.(name,args);
|
|
144
181
|
if (args.operation_id && name === 'agdf_record' && args.command?.kind === 'runtime_generation.replace'
|
|
145
182
|
&& operations.has(operationKey(args))) throw new DispatchExecutionError('operation_payload_conflict', 'not_dispatched');
|
|
146
183
|
if (args.operation_id && args.command?.kind !== 'runtime_generation.replace' && receipts.has(args.operation_id))
|
|
147
184
|
throw new DispatchExecutionError('operation_payload_conflict', 'not_dispatched');
|
|
148
185
|
if (name === 'agdf_record' && args.command?.kind === 'runtime_generation.replace'
|
|
149
186
|
|| name === 'agdf_inspect' && args.operation === 'runtime-generations') {
|
|
187
|
+
const controlRuntime = await getControlRuntime();
|
|
188
|
+
if (closed || signal?.aborted) throw new DispatchExecutionError(closed ? 'connection_closed' : 'dispatch_cancelled', 'not_dispatched');
|
|
189
|
+
try { controlRuntime.verifyProducer(); } catch (error) { throw new DispatchExecutionError(generationFailureReason(error), 'not_dispatched'); }
|
|
150
190
|
const result = await controlRuntime.tool(name).execute(controlRuntime.tool(name).parse(args));
|
|
151
191
|
return controlRuntime.projectToolResult(name, args, name === 'agdf_inspect' && result.report?.available ? { ...result, report: await control.refresh() } : result);
|
|
152
192
|
}
|
|
153
193
|
if (name === 'agdf_inspect' && args.operation === 'operation-status' && receipts.has(args.module)) {
|
|
194
|
+
const controlRuntime = await getControlRuntime();
|
|
195
|
+
if (closed || signal?.aborted) throw new DispatchExecutionError(closed ? 'connection_closed' : 'dispatch_cancelled', 'not_dispatched');
|
|
196
|
+
try { controlRuntime.verifyProducer(); } catch (error) { throw new DispatchExecutionError(generationFailureReason(error), 'not_dispatched'); }
|
|
154
197
|
const validated = await controlRuntime.tool(name).execute(controlRuntime.tool(name).parse(args));
|
|
155
198
|
return controlRuntime.projectToolResult(name, args, validated.report?.status === 'unknown' || validated.report?.status === 'absent'
|
|
156
199
|
? { ...validated, report: control.inspect(args.module, args) } : validated);
|
|
157
200
|
}
|
|
158
201
|
const knownSession = args.session_id && routes.get(args.session_id);
|
|
159
202
|
if (args.session_id && !knownSession) throw new DispatchExecutionError('session_expired', 'not_dispatched');
|
|
160
|
-
const id = name === 'agdf_inspect' && args.operation === 'operation-status' ? args.module : args.operation_id;
|
|
203
|
+
const id = name === 'agdf_inspect' && args.operation === 'operation-status' ? args.module.split('/').at(-1) : canonical ? args.operation_id : null;
|
|
161
204
|
const key = id && operationKey(args, id), original = key && operations.get(key);
|
|
162
205
|
const presentationId = args.command?.payload?.presentation_id ?? args.presentation_id;
|
|
163
206
|
const presentationOwner = presentationId && presentations.get(operationKey(args, presentationId));
|
|
164
207
|
const row = knownSession ?? original ?? presentationOwner ?? current;
|
|
165
208
|
if (!row || row.exited) throw new DispatchExecutionError('generation_unavailable', 'not_dispatched');
|
|
166
|
-
if (
|
|
209
|
+
if (canonical && !original) {
|
|
167
210
|
if (operations.size >= L.routes) throw new DispatchExecutionError('generation_route_capacity', 'not_dispatched');
|
|
168
211
|
operations.set(key, row);
|
|
169
212
|
}
|
|
170
213
|
++row.inflight; event(row, 'invoke', name);
|
|
171
214
|
try {
|
|
172
|
-
const result = (await request(row, 'invoke', { name, arguments: args }, actualContext, args.operation_id
|
|
173
|
-
if (
|
|
215
|
+
const result = (await request(row, 'invoke', { name, arguments: args }, actualContext, canonical ? args.operation_id : null, invokeMs)).payload;
|
|
216
|
+
if (canonical && result.outcome === 'effect_unknown') row.unknown.add(key);
|
|
174
217
|
if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
|
|
175
|
-
&& result.report.receipt?.operation_id
|
|
218
|
+
&& (result.report.receipt?.operation_id ?? result.report.receipt?.operation?.operation_id) === id) row.unknown.delete(key);
|
|
176
219
|
if (result.app_capability === 'available') nativeRequired = true;
|
|
177
220
|
if (result.family_result?.presentation_id && ['accepted', 'already_applied'].includes(result.outcome)) {
|
|
178
221
|
const presentationKey = operationKey(args, result.family_result.presentation_id);
|
|
@@ -185,7 +228,7 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
185
228
|
}
|
|
186
229
|
}
|
|
187
230
|
return result;
|
|
188
|
-
} catch (error) { if (
|
|
231
|
+
} catch (error) { if (canonical && error.dispatch !== 'not_dispatched') row.unknown.add(key); throw error; }
|
|
189
232
|
finally { --row.inflight; if (closed && !row.inflight && !row.unknown.size) void retire(row, true); }
|
|
190
233
|
}
|
|
191
234
|
current = await reserve(publisher.initial, {}); current.state = 'current'; event(current, 'admitted');
|
|
@@ -194,16 +237,17 @@ export async function createOwnedGenerationFacade({ runtime, publisher, createCo
|
|
|
194
237
|
// not wire requests; parsing them again in a child both breaks the contract
|
|
195
238
|
// and copies the entire locale registry into a bounded private message.
|
|
196
239
|
const tools = runtime.tools.map(tool => ({ ...tool, parentOwned: true,
|
|
197
|
-
parse: input => { tool.parse(input); return input; }, execute: input => invoke(tool.name, input) }));
|
|
240
|
+
parse: input => { if (runtime.validatePublicInput?.(tool.name, input) !== true) tool.parse(input); return input; }, execute: input => invoke(tool.name, input) }));
|
|
198
241
|
// The server supplies actual initialize/request context only at this seam.
|
|
199
242
|
for (const tool of tools) tool.execute = (input, signal, host) => invoke(tool.name, input, host, signal);
|
|
200
243
|
return Object.freeze({ ...runtime, tools, tool: name => tools.find(tool => tool.name === name),
|
|
201
|
-
requiresProjectedResult: true, postprocessToolResult: undefined, inspectOwnership: status, runtimeControl: control,
|
|
244
|
+
requiresProjectedResult: true, prepareInput: undefined, postprocessToolResult: undefined, inspectOwnership: status, runtimeControl: control,
|
|
202
245
|
close() {
|
|
203
246
|
return closing ??= (async () => {
|
|
204
247
|
closed = true;
|
|
205
248
|
const previous = current; current = null; if (previous && !previous.exited) previous.state = 'draining';
|
|
206
249
|
await Promise.allSettled([...slots.values()].map(row => retire(row, true)));
|
|
250
|
+
if (controlRuntimePromise) await controlRuntimePromise.then(controlRuntime => controlRuntime.close?.(), () => {});
|
|
207
251
|
return status();
|
|
208
252
|
})();
|
|
209
253
|
},
|
package/src/main.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import { createAgdfWorkerExecutor, serveAgdfStdio } from "./server.js";
|
|
4
3
|
import { loadCockpitResource } from './cockpit-resource.js';
|
|
5
4
|
|
|
6
|
-
export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime, executor, ownedPublication, ownedPublisher, onClosed } = {}) {
|
|
5
|
+
export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime, executor, ownedPublication, ownedPublisher, generationTransport, onClosed } = {}) {
|
|
7
6
|
if (cockpitMode !== undefined && (cockpitMode !== 'plugin' || surface !== 'codex' || cockpitDir !== undefined)) throw Error('AGDF_COCKPIT_TARGET_INVALID');
|
|
8
7
|
const defaultRuntime = runtime === undefined && cockpitDir === undefined && cockpitMode === undefined;
|
|
9
8
|
// A supplied, bound runtime owns its resources. Do not load a second package
|
|
10
9
|
// composition merely to run an explicit adapter/protocol fixture.
|
|
11
|
-
const
|
|
12
|
-
|
|
10
|
+
const thin = defaultRuntime && !!ownedPublication;
|
|
11
|
+
const dispatcherRuntime = runtime === undefined ? await import(thin ? 'create-agdf/mcp-protocol-runtime' : 'create-agdf/mcp-dispatch-runtime') : null;
|
|
12
|
+
runtime ??= cockpitDir !== undefined || cockpitMode !== undefined ? await dispatcherRuntime.createMcpCockpitRuntime({ surface, cockpitDir, cockpitMode })
|
|
13
|
+
: thin ? dispatcherRuntime.createMcpTransportRuntime({ surface }) : dispatcherRuntime.createMcpDispatchRuntime({ surface });
|
|
13
14
|
const serverVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
14
15
|
if (runtime.trustedContext.expectedVersion !== serverVersion) {
|
|
15
16
|
process.stderr.write("AGDF_MCP_VERSION_MISMATCH\n");
|
|
@@ -21,26 +22,58 @@ export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime,
|
|
|
21
22
|
process.exitCode = 1;
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
24
|
-
if (
|
|
25
|
+
if (ownedPublication) {
|
|
26
|
+
// The serving protocol factory freshly inspects all selected source and SDK
|
|
27
|
+
// bytes. Bind that actual producer to the parent's claimed expectation;
|
|
28
|
+
// never treat a cross-thread/async observation as fresh inspection evidence.
|
|
29
|
+
const producer = runtime.producerIdentity;
|
|
30
|
+
if (!producer || producer.server_digest !== ownedPublication.digest
|
|
31
|
+
|| producer.dispatcher_digest !== ownedPublication.dispatcherDigest || producer.sdk_digest !== ownedPublication.sdkDigest)
|
|
32
|
+
throw Error('AGDF_MCP_FACADE_IDENTITY_INVALID');
|
|
33
|
+
}
|
|
34
|
+
if (defaultRuntime && surface === 'codex') {
|
|
25
35
|
const ui = { uri: dispatcherRuntime.DECISION_UI_URI, mimeType: 'text/html;profile=mcp-app', limit: 4 * 1024 ** 2, kind: 'decision' };
|
|
26
|
-
|
|
36
|
+
// The thin front has no session/qualification consumer. Its facade loads
|
|
37
|
+
// the resource for the real generation manifest; full runtimes need it here.
|
|
38
|
+
if (!thin) ui.digest = loadCockpitResource(ui).digest;
|
|
27
39
|
// A shipped resource does not qualify native App-only mediation. Production
|
|
28
40
|
// actions remain disabled until exact package/host evidence is provided.
|
|
29
|
-
runtime = await dispatcherRuntime.
|
|
41
|
+
runtime = thin ? await dispatcherRuntime.createMcpDecisionTransportRuntime({ runtime, ui })
|
|
42
|
+
: await dispatcherRuntime.createMcpDecisionRuntime({ runtime, ui });
|
|
30
43
|
}
|
|
31
44
|
if (runtime.mode === 'cockpit') loadCockpitResource(runtime.ui);
|
|
45
|
+
let transport;
|
|
32
46
|
if (ownedPublication) {
|
|
33
47
|
const { createOwnedGenerationFacade } = await import('./generation.js');
|
|
34
48
|
if (!ownedPublisher) throw Error('runtime_publication_unowned');
|
|
35
49
|
const publisher = ownedPublisher;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
let beginTransport;
|
|
51
|
+
const claimed = new Promise(resolve => { beginTransport = resolve; });
|
|
52
|
+
const starting = createOwnedGenerationFacade({ runtime, publisher, generationTransport, surface, cockpitDir, cockpitMode,
|
|
53
|
+
onEvent: event => { if (event.kind === 'qualifying') beginTransport(); },
|
|
54
|
+
createControlRuntime: async runtimeControl => {
|
|
55
|
+
const full = thin ? await import('create-agdf/mcp-dispatch-runtime') : dispatcherRuntime;
|
|
56
|
+
return full.createMcpDispatchRuntime({ surface, runtimeControl });
|
|
57
|
+
} });
|
|
58
|
+
// Only a claimed private-generation start releases the SDK import. A
|
|
59
|
+
// pre-claim failure rejects starting without loading transport code. Its full
|
|
60
|
+
// qualification and the facade SDK must both finish before public service.
|
|
61
|
+
// Observe both promises so a transport failure cannot abandon a live child.
|
|
62
|
+
const transportStarting = Promise.race([claimed, starting]).then(() => import('./server.js'));
|
|
63
|
+
const [generation, loaded] = await Promise.allSettled([starting, transportStarting]);
|
|
64
|
+
if (generation.status === 'rejected' || loaded.status === 'rejected') {
|
|
65
|
+
if (generation.status === 'fulfilled') await generation.value.close();
|
|
66
|
+
throw generation.status === 'rejected' ? generation.reason : loaded.reason;
|
|
67
|
+
}
|
|
68
|
+
runtime = generation.value; transport = loaded.value;
|
|
69
|
+
} else transport = await import('./server.js');
|
|
70
|
+
const { createAgdfWorkerExecutor, serveAgdfStdio } = transport;
|
|
39
71
|
const dispatchExecutor = executor ?? (runtime.mode === 'cockpit' ? undefined
|
|
40
72
|
: createAgdfWorkerExecutor(runtime, { releaseOwnedLock: dispatcherRuntime?.releaseAbandonedOwnedLock ?? null }));
|
|
41
73
|
const handle = serveAgdfStdio({
|
|
42
74
|
runtime,
|
|
43
75
|
executor: dispatchExecutor,
|
|
76
|
+
preparedCallStore: ownedPublication ? ownedPublisher?.preparedCallStore?.({ surface }) : undefined,
|
|
44
77
|
onerror(code) {
|
|
45
78
|
process.stderr.write(`${code}\n`);
|
|
46
79
|
},
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const writeTools = new Set(['agdf_project', 'agdf_run', 'agdf_prepare', 'agdf_record', 'agdf_approve', 'agdf_revise', 'agdf_closeout']);
|
|
2
|
+
const knownTools = new Set([...writeTools, 'agdf_inspect', 'agdf_dispatch']);
|
|
3
|
+
const clone = value => structuredClone(value);
|
|
4
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
const canonical = value => JSON.stringify(stable(value));
|
|
6
|
+
function stable(value) {
|
|
7
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
8
|
+
return object(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])) : value;
|
|
9
|
+
}
|
|
10
|
+
function callValue(value) {
|
|
11
|
+
return object(value) && knownTools.has(value.tool) && object(value.arguments)
|
|
12
|
+
? { tool: value.tool, arguments: clone(value.arguments) } : null;
|
|
13
|
+
}
|
|
14
|
+
function formValue(value) {
|
|
15
|
+
if (!object(value)) return null;
|
|
16
|
+
const call = callValue(value.call ?? value);
|
|
17
|
+
if (!call) return null;
|
|
18
|
+
// A normal mutating action must already be the transport's bound form. Never
|
|
19
|
+
// promote its Raw recovery branch or reconstruct one from operation metadata.
|
|
20
|
+
if (writeTools.has(call.tool) && (!Object.hasOwn(call.arguments, 'prepared_call_id')
|
|
21
|
+
|| typeof call.arguments.prepared_call_id !== 'string' || !call.arguments.prepared_call_id
|
|
22
|
+
|| !object(call.arguments.inputs) || Object.keys(call.arguments).some(key => !['prepared_call_id', 'inputs'].includes(key)))) return null;
|
|
23
|
+
if (value.required_inputs !== undefined && (!Array.isArray(value.required_inputs)
|
|
24
|
+
|| value.required_inputs.some(field => typeof field !== 'string' || !field))) return null;
|
|
25
|
+
const source = clone(value);
|
|
26
|
+
delete source.tool; delete source.arguments;
|
|
27
|
+
return { ...source, call, required_inputs: Array.isArray(value.required_inputs) ? clone(value.required_inputs) : [] };
|
|
28
|
+
}
|
|
29
|
+
function uniqueForms(values) {
|
|
30
|
+
const forms = new Map();
|
|
31
|
+
for (const value of values.filter(Boolean)) {
|
|
32
|
+
const form = formValue(value);
|
|
33
|
+
if (!form) return null;
|
|
34
|
+
const key = canonical({ call: form.call, required_inputs: form.required_inputs });
|
|
35
|
+
if (!forms.has(key)) forms.set(key, form);
|
|
36
|
+
}
|
|
37
|
+
return [...forms.values()];
|
|
38
|
+
}
|
|
39
|
+
function base(state, instruction, source, extra = {}) {
|
|
40
|
+
return { state, call: null, required_inputs: [], instruction, source, ...extra, authorizes: false };
|
|
41
|
+
}
|
|
42
|
+
function terminal(result, source) {
|
|
43
|
+
return base('terminal', 'Respect this result\'s terminal host_action and stop; no next tool call is selected.', source,
|
|
44
|
+
result.host_action ? { host_action: clone(result.host_action) } : {});
|
|
45
|
+
}
|
|
46
|
+
function selectForms(values, source, { forceReply = false, readOnly = false } = {}) {
|
|
47
|
+
const forms = uniqueForms(values);
|
|
48
|
+
if (!forms?.length) return base('requires_source', 'No complete normal action was supplied. Read the canonical source or use its explicit recovery instructions; do not construct a replacement call.', source);
|
|
49
|
+
if (forms.length !== 1) return base('requires_selection', 'Several distinct actions were supplied. Preserve the current binding and obtain the authoritative selection; do not pick the first action.', source);
|
|
50
|
+
const form = forms[0];
|
|
51
|
+
if (readOnly && form.call.tool !== 'agdf_inspect') return base('requires_source', 'The supplied continuation is not a read-only page call; no action is selected.', source);
|
|
52
|
+
const reply = forceReply || form.call.tool === 'agdf_approve' || form.operation?.kind === 'approval.record';
|
|
53
|
+
if (reply) {
|
|
54
|
+
// The contract's approval_value is syntax, never a response. A producer that
|
|
55
|
+
// filled a reply in an offered form cannot establish new human intent here.
|
|
56
|
+
if (Object.values(form.call.arguments.inputs ?? {}).some(value => value !== '' && value !== undefined))
|
|
57
|
+
return base('requires_new_reply', 'Obtain a fresh blank approval form and a NEW deliberate reply for its exact presentation. No reply or approval is inferred.', source);
|
|
58
|
+
return { ...form, state: 'requires_new_reply', source, authorizes: false,
|
|
59
|
+
instruction: 'Deliver the current presentation before waiting for a NEW deliberate reply. Fill only the supplied reply field from that response; never copy approval_value as a decision. ' + (form.instruction ?? '') };
|
|
60
|
+
}
|
|
61
|
+
return { ...form, state: form.required_inputs.length ? 'form' : 'ready', source, authorizes: false,
|
|
62
|
+
instruction: 'Use this one exact call. Supply only its named required inputs from actual authored content or review; do not rebuild IDs, bindings or a Raw envelope. ' + (form.instruction ?? '') };
|
|
63
|
+
}
|
|
64
|
+
function authoringKey(result, report) {
|
|
65
|
+
if (report.format !== 'document_bundle_v2' || !/^sha256:[0-9a-f]{64}$/u.test(report.manifest_digest ?? '')) return null;
|
|
66
|
+
return canonical({ manifest: report.manifest_digest,
|
|
67
|
+
target_id: result.target?.target_id ?? result.binding?.target_id ?? report.target_id ?? null,
|
|
68
|
+
target: result.target?.governance_target ?? result.target?.display_path ?? null,
|
|
69
|
+
run_id: report.run_id ?? null, revision_id: report.revision_id ?? null, planning: report.planning ?? null,
|
|
70
|
+
owner: report.authoring_owner ?? null, artifact_language: report.artifact_language ?? null,
|
|
71
|
+
presentation_language: report.presentation_language ?? null });
|
|
72
|
+
}
|
|
73
|
+
function authoringRevision(key) {
|
|
74
|
+
const { manifest, ...binding } = JSON.parse(key);
|
|
75
|
+
return canonical(binding);
|
|
76
|
+
}
|
|
77
|
+
function authoringScope(key) {
|
|
78
|
+
const { manifest, revision_id, ...scope } = JSON.parse(key);
|
|
79
|
+
return canonical(scope);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A connection-local projection of supplied actions, not an executor, permission
|
|
83
|
+
// decision or proof that the model has read/reviewed all source pages.
|
|
84
|
+
export function createNextActionProjection({ maxEntries = 32, isAvailable = () => true } = {}) {
|
|
85
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1 || maxEntries > 32) throw Error('next_action_cache_limit_invalid');
|
|
86
|
+
if (typeof isAvailable !== 'function') throw Error('next_action_availability_invalid');
|
|
87
|
+
const forms = new Map(), superseded = new Set();
|
|
88
|
+
function available(form) {
|
|
89
|
+
const normal = formValue(form);
|
|
90
|
+
if (!normal) return false;
|
|
91
|
+
try { return isAvailable(clone(normal.call)) === true; } catch { return false; }
|
|
92
|
+
}
|
|
93
|
+
function remember(key, form) {
|
|
94
|
+
if (!key) return;
|
|
95
|
+
const revision = authoringRevision(key);
|
|
96
|
+
if (superseded.has(revision)) return;
|
|
97
|
+
if (!available(form)) { forms.delete(key); return; }
|
|
98
|
+
// A newly supplied first-page form for this exact authoring scope replaces
|
|
99
|
+
// cached forms from its earlier revision. Never refresh their bindings, or
|
|
100
|
+
// let a later old page revive them. Other targets/Runs/owners stay intact.
|
|
101
|
+
const scope = authoringScope(key);
|
|
102
|
+
for (const prior of forms.keys()) if (authoringScope(prior) === scope && authoringRevision(prior) !== revision) {
|
|
103
|
+
forms.delete(prior); superseded.add(authoringRevision(prior));
|
|
104
|
+
}
|
|
105
|
+
while (superseded.size > maxEntries) superseded.delete(superseded.values().next().value);
|
|
106
|
+
forms.delete(key); forms.set(key, clone(form));
|
|
107
|
+
while (forms.size > maxEntries) forms.delete(forms.keys().next().value);
|
|
108
|
+
}
|
|
109
|
+
function rememberAuthoring(result) {
|
|
110
|
+
if (result?.terminal === true || result?.outcome !== 'inspect_result' || result.operation !== 'authoring-inputs') return;
|
|
111
|
+
const report = result.report;
|
|
112
|
+
if (!object(report) || report.available === false || !report.material_call
|
|
113
|
+
|| report.page_index !== undefined && report.page_index !== 0) return;
|
|
114
|
+
for (const owner of [result, result.continuation, result.navigation, report, result.family_result].filter(object))
|
|
115
|
+
if (owner.next_calls !== undefined && owner.next_calls !== null && !Array.isArray(owner.next_calls)
|
|
116
|
+
|| owner.recording_calls !== undefined && owner.recording_calls !== null && !Array.isArray(owner.recording_calls)) return;
|
|
117
|
+
remember(authoringKey(result, report), report.material_call);
|
|
118
|
+
}
|
|
119
|
+
function fromContext(result, source) {
|
|
120
|
+
if (result.terminal === true) return terminal(result, source);
|
|
121
|
+
if (['rejected', 'recovery_required', 'effect_unknown', 'invalid_input', 'evaluator_error', 'target_unresolved'].includes(result.outcome))
|
|
122
|
+
return base('requires_source', 'This operation did not establish a successful normal continuation. Preserve its original receipt and recovery instructions; no write form is selected.', source);
|
|
123
|
+
const report = object(result.report) ? result.report : null;
|
|
124
|
+
for (const owner of [result, result.continuation, result.navigation, report, result.family_result].filter(object))
|
|
125
|
+
if (owner.next_calls !== undefined && owner.next_calls !== null && !Array.isArray(owner.next_calls)
|
|
126
|
+
|| owner.recording_calls !== undefined && owner.recording_calls !== null && !Array.isArray(owner.recording_calls))
|
|
127
|
+
return base('requires_source', 'The supplied action list is malformed. Preserve the existing outcome and obtain the canonical action source.', source);
|
|
128
|
+
if (Object.hasOwn(result, 'material_call')) {
|
|
129
|
+
const material = formValue(result.material_call);
|
|
130
|
+
// This is an explicit correction after accepted private staging, not a
|
|
131
|
+
// ready write or permission to bypass the source's recording decision.
|
|
132
|
+
if (result.outcome !== 'accepted' || result.historical_effect === true || result.source_validation?.recordable !== false
|
|
133
|
+
|| report?.available === false || report?.complete === false
|
|
134
|
+
|| !material || material.call.tool !== 'agdf_record' || material.operation?.kind !== 'material.stage'
|
|
135
|
+
|| material.state === 'transport_unavailable' || material.transport_reason
|
|
136
|
+
|| !material.required_inputs.length || new Set(material.required_inputs).size !== material.required_inputs.length
|
|
137
|
+
|| Object.keys(material.call.arguments.inputs).length || !available(material))
|
|
138
|
+
return base('requires_source', 'No complete authored correction form was supplied. Preserve the source validation and receipt; obtain the canonical correction source without constructing a new call.', source + '.material_call');
|
|
139
|
+
const competing = [...(result.recording_calls ?? []), ...(result.next_calls ?? []),
|
|
140
|
+
...(result.continuation?.next_calls ?? []), ...(report?.next_calls ?? []),
|
|
141
|
+
...(result.navigation?.next_calls ?? []), ...(result.family_result?.next_calls ?? []),
|
|
142
|
+
...(result.approval_call ? [result.approval_call] : []), ...(report?.prepared_call ? [report.prepared_call] : []),
|
|
143
|
+
...(report?.material_call ? [report.material_call] : [])];
|
|
144
|
+
if (competing.some(value => !value))
|
|
145
|
+
return base('requires_source', 'The correction action list is malformed. Preserve the source validation and obtain the canonical correction source.', source + '.material_call');
|
|
146
|
+
return selectForms([result.material_call, ...competing], source + '.material_call');
|
|
147
|
+
}
|
|
148
|
+
if (result.approval_call && result.presentation_delivery && !result.historical_effect && !result.presentation_delivery.historical_delivery
|
|
149
|
+
&& ['accepted', 'already_applied'].includes(result.outcome)) return selectForms([result.approval_call], source + '.approval_call', { forceReply: true });
|
|
150
|
+
if (Array.isArray(result.recording_calls) && result.recording_calls.length)
|
|
151
|
+
return selectForms(result.recording_calls, source + '.recording_calls');
|
|
152
|
+
if (result.operation === 'operation-contract' && report?.prepared_call && report.available === false)
|
|
153
|
+
return base('requires_source', 'The operation-contract report is unavailable. Preserve its original binding and recovery instructions; no write form is selected.', source + '.report');
|
|
154
|
+
if (result.operation === 'operation-contract' && report?.prepared_call)
|
|
155
|
+
return selectForms([report.prepared_call], source + '.report.prepared_call');
|
|
156
|
+
if (result.operation === 'authoring-inputs' && report) {
|
|
157
|
+
if (report.available === false)
|
|
158
|
+
return base('requires_source', 'The authoring source is unavailable. Preserve its original binding and recovery instructions; no staging form is selected.', source + '.report');
|
|
159
|
+
const key = authoringKey(result, report);
|
|
160
|
+
if (report.material_call && (report.page_index === 0 || report.page_index === undefined)) remember(key, report.material_call);
|
|
161
|
+
if (key && superseded.has(authoringRevision(key)))
|
|
162
|
+
return base('requires_source', 'A newly supplied first-page form superseded this authoring revision. Preserve the selected source and obtain its canonical current form; no older binding or cached form is revived.', source + '.report');
|
|
163
|
+
if (report.complete !== true) {
|
|
164
|
+
if (report.next_calls?.length) {
|
|
165
|
+
const next = selectForms(report.next_calls, source + '.report.next_calls', { readOnly: true });
|
|
166
|
+
return next.state === 'ready' ? { ...next, state: 'read_required',
|
|
167
|
+
instruction: 'Read this exact next page before authoring. Retain earlier bytes and respect fragment identities; page transport does not prove review. ' + next.instruction } : next;
|
|
168
|
+
}
|
|
169
|
+
return base('requires_source', 'The authoring source is incomplete and no exact next-page call was supplied. Do not stage or edit incomplete content.', source + '.report');
|
|
170
|
+
}
|
|
171
|
+
if (report.authoring_scope === 'delivery-closeout')
|
|
172
|
+
return base('idle', 'The bound closeout evidence is complete. Use the current control and evidence for the requested handoff.', source + '.report');
|
|
173
|
+
let material = report.material_call ?? (key && forms.get(key));
|
|
174
|
+
if (material && !available(material)) {
|
|
175
|
+
if (key) forms.delete(key);
|
|
176
|
+
material = null;
|
|
177
|
+
}
|
|
178
|
+
if (material) return selectForms([material], source + (report.material_call ? '.report.material_call' : '.cached_material_call'));
|
|
179
|
+
if (report.first_page_call) {
|
|
180
|
+
const next = selectForms([report.first_page_call], source + '.report.first_page_call', { readOnly: true });
|
|
181
|
+
return next.state === 'ready' ? { ...next, state: 'read_required',
|
|
182
|
+
instruction: 'No first-page form is retained for this exact manifest. Read the supplied first page; never derive a cursor, reference or new binding. ' + next.instruction } : next;
|
|
183
|
+
}
|
|
184
|
+
return base('requires_source', 'No authoring form is available for this exact manifest. Obtain its canonical first page; do not reuse another target, revision or manifest.', source + '.report');
|
|
185
|
+
}
|
|
186
|
+
const supplied = [...(result.next_calls ?? []), ...(result.continuation?.next_calls ?? []),
|
|
187
|
+
...(report?.next_calls ?? []), ...(result.navigation?.next_calls ?? []), ...(result.family_result?.next_calls ?? [])];
|
|
188
|
+
if (supplied.length) return selectForms(supplied, source + '.next_calls');
|
|
189
|
+
const resume = [...(result.resume_call ? [result.resume_call] : []),
|
|
190
|
+
...(result.continuation?.resume_call ? [result.continuation.resume_call] : [])];
|
|
191
|
+
if (resume.length) return selectForms(resume, source + '.resume_call');
|
|
192
|
+
if ([result, result.continuation, report].filter(object).some(owner => owner.recovery_only?.unavailable_calls?.length))
|
|
193
|
+
return base('requires_source', 'No normal call could be registered. The exact original requests are retained only in recovery_only; follow original-operation recovery without creating new identities.', source);
|
|
194
|
+
return base('idle', 'No unique next action was supplied. Follow the canonical current-state instruction; no tool call is inferred.', source);
|
|
195
|
+
}
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
project(domain) {
|
|
198
|
+
const result = clone(domain);
|
|
199
|
+
// An adapter may already have loaded one safe page after this result.
|
|
200
|
+
// Retain the exact outer first-page form before selecting that nested
|
|
201
|
+
// context; incomplete nested pages still select only their supplied read.
|
|
202
|
+
rememberAuthoring(result);
|
|
203
|
+
if (result.terminal === true) result.next_action = terminal(result, 'outer');
|
|
204
|
+
else if (result.next_read) {
|
|
205
|
+
const read = result.next_read, nested = read.result;
|
|
206
|
+
// An explicit null means the exact read could not be retained. Its Raw
|
|
207
|
+
// execution record remains evidence/recovery, never a normal fallback.
|
|
208
|
+
const retry = Object.hasOwn(read, 'call') ? read.call : read;
|
|
209
|
+
if (read.failure?.phase === 'validation') result.next_action = base('requires_source',
|
|
210
|
+
'The supplied read could not be validated. Preserve the original outcome and receipt; obtain the canonical read source without repeating the write.', 'next_read');
|
|
211
|
+
else if (nested?.terminal === true) result.next_action = terminal(nested, 'next_read.result');
|
|
212
|
+
else if (!read.error && read.failed !== true && nested?.outcome === 'inspect_result' && nested.report?.available !== false)
|
|
213
|
+
result.next_action = fromContext(nested, 'next_read.result');
|
|
214
|
+
else if (callValue(retry)?.tool === 'agdf_inspect') {
|
|
215
|
+
const next = selectForms([retry], 'next_read', { readOnly: true });
|
|
216
|
+
result.next_action = next.state === 'ready' ? { ...next, state: 'read_required',
|
|
217
|
+
instruction: 'The follow-up read did not produce an available nonterminal result. Retry only this exact read when appropriate; never repeat the outer write. ' + next.instruction } : next;
|
|
218
|
+
} else result.next_action = base('requires_source', 'The follow-up read failed without an exact supplied read call. Preserve the outer outcome/receipt; do not retry the outer write.', 'next_read');
|
|
219
|
+
} else result.next_action = fromContext(result, 'outer');
|
|
220
|
+
result.next_action.context_path = result.next_read?.result ? 'next_read.result' : 'outer';
|
|
221
|
+
if (result.next_read?.result?.report) result.next_action.instruction +=
|
|
222
|
+
' The already loaded source report is response.structuredContent.next_read.result.report. Consume that report without another inspect call.';
|
|
223
|
+
return result;
|
|
224
|
+
},
|
|
225
|
+
close() { forms.clear(); superseded.clear(); },
|
|
226
|
+
});
|
|
227
|
+
}
|