@agdf/mcp-server 0.30.0 → 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 +26 -2
- package/package.json +2 -2
- package/src/generation-runtime.js +11 -2
- package/src/main.js +1 -0
- 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 +139 -35
- package/ui/cockpit.html +2 -2
- package/ui/decision-manifest.json +3 -3
- package/ui/decision.html +2 -2
- package/ui/manifest.json +3 -3
package/README.md
CHANGED
|
@@ -85,8 +85,18 @@ Unknown/unowned partial states remain blocked. Successful setup returns creation
|
|
|
85
85
|
`agdf_run/run.create` creates an unapproved Run from a semantic title and original operation UUID;
|
|
86
86
|
pre-Run `operation-status` resolves a lost response without inventing a second Run.
|
|
87
87
|
`authoring-inputs` returns versioned templates and exact sources with bounded cursors.
|
|
88
|
-
`
|
|
89
|
-
|
|
88
|
+
`authoring-inputs.material_call` supplies a bound form: fill its UTF-8 content and use
|
|
89
|
+
`agdf_record/material.stage` action `put` to seal files of at most 48 KiB each within the 64 KiB whole JSON request bound in one call.
|
|
90
|
+
The Core computes byte counts, material identities and digests. Larger files retain the bounded
|
|
91
|
+
multipart path (16 KiB per part, 128 KiB per file). Each multipart action has its own operation UUID;
|
|
92
|
+
only an identical retry reuses a UUID. Returned `recording_calls` provide the binding and material
|
|
93
|
+
reference; fill only the listed author/review fields. Ready `next_calls` are executable as supplied;
|
|
94
|
+
do not add intake/revision fields to a supplied `continue_delivery` dispatch.
|
|
95
|
+
Pass authored Markdown as UTF-8 string data through structured input. If a host uses JavaScript,
|
|
96
|
+
author with its file-editing tool and pass the UTF-8 file read as the content value; embedding
|
|
97
|
+
the whole document in a backtick literal makes Markdown syntax executable JavaScript syntax.
|
|
98
|
+
This guidance adds no server-side file reader and changes no byte or input limit.
|
|
99
|
+
These Core-owned references feed existing typed writers, including reviewed formal artefacts, unapproved
|
|
90
100
|
UR replacement, Verified Change and closeout. Code edits, actual tests and Git execution remain
|
|
91
101
|
external work; none of these operations grants approval.
|
|
92
102
|
|
|
@@ -246,3 +256,17 @@ They do not qualify an installed host or another OS. The fixed performance test
|
|
|
246
256
|
the actual owned plugin launcher with 20/20 samples and p95 limits of 2500 ms cold /
|
|
247
257
|
1000 ms warm. Performance acceptance and native qualification are separate
|
|
248
258
|
required evidence, not consequences of the topology declaration.
|
|
259
|
+
|
|
260
|
+
The normal MCP path is `next_action.call`: either a ready call forwarded unchanged or a form with named `required_inputs` such as `content`, `title`, `reviewer`, `reviewed` or `response`. The adapter maps those names to the stored canonical payload fields. It retains the original target, Run, revision, permission group and operation UUID; it never fills authored content, review attestations or approval replies. The former competing `submit` form is absent. Raw requests remain accepted for compatibility, but the full original request is exposed only under the explicit `recovery_only` branch, with the exact field map and original UUID. Retain that branch and the actual submitted inputs for uncertain-outcome recovery; do not generate a replacement identity.
|
|
261
|
+
|
|
262
|
+
Verified owned plugin launchers retain offered templates in a private bounded journal under their existing MCP data root. The namespace includes the serving surface, version and server/dispatcher/SDK identity. A reconnect at that same identity can resolve the original reference without a new UUID or refreshed revision. The original 30-minute lifetime is never renewed. No submitted author/review/reply inputs or execution receipts are added to this journal; it is transport storage, not canonical Run State or authority. Direct transports without this verified owner remain connection-local. Private material buffers have their separate existing generation lifetime and still require exact semantic-byte recovery after generation loss.
|
|
263
|
+
|
|
264
|
+
Empty, malformed, unknown and known-expired references remain distinct. An unavailable live reference returns `prepared_call_unknown`, rather than blaming the authored command schema. Store failures are distinguished from missing references. A failed publication of a future reference never discards an already successful operation receipt; its exact original request remains in recovery. Every restored call still passes the current SDK, permission-group, Core and revision checks.
|
|
265
|
+
|
|
266
|
+
For a unique Core-supplied normal next step, the adapter may execute one exact `agdf_inspect` call for `authoring-inputs`, `gate-check` or `operation-contract` and include it as `next_read.result`. `next_action.context_path` names that already-loaded context. The original operation outcome and receipt remain intact if this separate read fails. No dispatch, write, presentation or approval is chained automatically; `agdf_dispatch` is not read-only. Source pagination remains explicit. Internal read attempts/failures are accounted separately from outer MCP invocations in native observations.
|
|
267
|
+
|
|
268
|
+
`authoring-inputs` supports `bundle:true` for bounded multi-document pages. Read every chunk and cursor without a duplicate writer-contract lookup. A retained first-page form is reusable only for the same exact manifest and an available connection reference; otherwise use the supplied first-page read. Page transport does not prove source review.
|
|
269
|
+
|
|
270
|
+
Prepared references also retain explicitly offered `agdf_inspect` reads, including source pages, contract help and original-operation status. Read inputs stay `{}`; the original SDK schema validates the complete supplied request before registration. Target, Run, revision, cursor and original status selection are never inferred or refreshed. The actual `next_read.tool/arguments` remain the record of an already executed read; its separate `next_read.call` is the exact prepared retry. Explicit recovery reads remain Raw for reconnect, and dispatch retains its original schema. No reference changes permission or bypasses canonical validation.
|
|
271
|
+
|
|
272
|
+
Bundled authoring reports use `document_bundle_v2`: `documents` contains complete text, `document_manifest` is metadata only, and `fragments` must be joined by identity and byte offsets across all cursors before editing. Missing content must never be coerced into a replacement string.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agdf/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Local MCP adapter for version-matched AGDF dispatch and control inspection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"scripts": {},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/server": "2.0.0",
|
|
26
|
-
"create-agdf": "0.
|
|
26
|
+
"create-agdf": "0.31.0"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=22"
|
|
@@ -12,6 +12,11 @@ export function startGenerationRuntime({ epoch, generation, channel }) {
|
|
|
12
12
|
return { ...owned, inflight: inflight + (starting ? 1 : 0) + (owned.inflight ?? 0),
|
|
13
13
|
executor: executor?.active ? 1 : 0, uncertain: unknown.size + (owned.uncertain ?? 0), closing };
|
|
14
14
|
}
|
|
15
|
+
// Companion events (SD SDD-006): this generation executed the call, so it journals the final result
|
|
16
|
+
// once, before the result leaves; a failing journal never changes or delays the result.
|
|
17
|
+
function journal(name, args, projected) {
|
|
18
|
+
try { runtime.observeOutcome?.(name, args, projected); } catch { /* notification only */ }
|
|
19
|
+
}
|
|
15
20
|
function send(input, kind, payload) {
|
|
16
21
|
const reply = generationMessage(epoch, generation, kind, payload, {}, input.original_operation_id, input.request_id);
|
|
17
22
|
if (validGenerationMessage(reply, { epoch, generation, reply: true })) channel.send(reply);
|
|
@@ -76,11 +81,15 @@ export function startGenerationRuntime({ epoch, generation, channel }) {
|
|
|
76
81
|
if (input.original_operation_id && result.outcome === 'effect_unknown') unknown.add(input.original_operation_id);
|
|
77
82
|
if (name === 'agdf_inspect' && args.operation === 'operation-status' && result.report?.status === 'recorded'
|
|
78
83
|
&& (result.report.receipt?.operation_id ?? result.report.receipt?.operation?.operation_id) === args.module.split('/').at(-1)) unknown.delete(args.module.split('/').at(-1));
|
|
79
|
-
|
|
84
|
+
const projected = runtime.projectToolResult(name, args, result);
|
|
85
|
+
journal(name, args, projected);
|
|
86
|
+
send(input, 'result', projected);
|
|
80
87
|
} catch (error) {
|
|
81
88
|
const facts = runtime.normalizeFailure(error, { phase });
|
|
82
89
|
if (input.original_operation_id && facts.dispatch !== 'not_dispatched') unknown.add(input.original_operation_id);
|
|
83
|
-
|
|
90
|
+
const projected = runtime.projectToolResult(name, args, runtime.failure(facts.code, name, args, { ...facts, elapsed_ms: performance.now() - started }), facts);
|
|
91
|
+
journal(name, args, projected);
|
|
92
|
+
send(input, 'result', projected);
|
|
84
93
|
} finally { --inflight; }
|
|
85
94
|
}
|
|
86
95
|
} catch (error) { if (input.kind === 'qualify') starting = false; send(input, 'failure', { reason: generationFailureReason(error) }); }
|
package/src/main.js
CHANGED
|
@@ -73,6 +73,7 @@ export async function runMcpServer({ surface, cockpitDir, cockpitMode, runtime,
|
|
|
73
73
|
const handle = serveAgdfStdio({
|
|
74
74
|
runtime,
|
|
75
75
|
executor: dispatchExecutor,
|
|
76
|
+
preparedCallStore: ownedPublication ? ownedPublisher?.preparedCallStore?.({ surface }) : undefined,
|
|
76
77
|
onerror(code) {
|
|
77
78
|
process.stderr.write(`${code}\n`);
|
|
78
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
|
+
}
|