@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/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
- const claim = publisher.acquire(packageInfo, { ownerEpoch: epoch, generation: row.id });
68
- row.child = Fork(join(packageInfo.packageRoot, 'src/generation-entry.js'), [epoch, String(row.id)],
69
- { stdio: ['ignore', 'ignore', 'ignore', 'ipc'], serialization: 'json', execArgv: [] });
70
- claim.bindOwnedChild(row.child, { confirmedEnd: () => row.closeAck && !row.inflight && !row.unknown.size });
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
- revalidate(); const packageInfo = publisher.resolve(candidate);
126
- candidateRow = await reserve(packageInfo, actualContext); revalidate();
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
- const controlRuntime = createControlRuntime(control);
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 (args.operation_id && !original) {
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 ?? null, invokeMs)).payload;
173
- if (args.operation_id && result.outcome === 'effect_unknown') row.unknown.add(key);
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 === args.module) row.unknown.delete(key);
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 (args.operation_id && error.dispatch !== 'not_dispatched') row.unknown.add(key); throw error; }
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 dispatcherRuntime = runtime === undefined ? await import('create-agdf/mcp-dispatch-runtime') : null;
12
- runtime ??= cockpitDir !== undefined || cockpitMode !== undefined ? await dispatcherRuntime.createMcpCockpitRuntime({ surface, cockpitDir, cockpitMode }) : dispatcherRuntime.createMcpDispatchRuntime({ surface });
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 (defaultRuntime && surface === 'codex' && dispatcherRuntime?.createMcpDecisionRuntime) {
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
- ui.digest = loadCockpitResource(ui).digest;
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.createMcpDecisionRuntime({ runtime, ui });
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
- runtime = await createOwnedGenerationFacade({ runtime, publisher, surface, cockpitDir, cockpitMode,
37
- createControlRuntime: runtimeControl => dispatcherRuntime.createMcpDispatchRuntime({ surface, runtimeControl }) });
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
+ }