generalize continuity plugin engine and generic adapter

This commit is contained in:
Eve
2026-04-24 19:28:20 +08:00
parent ed4fe3ea6c
commit 1fe6474009
13 changed files with 679 additions and 153 deletions

View File

@@ -1,3 +1,7 @@
import {
normalizeContinuityEngineInput,
createContinuityEngineContract,
} from '../continuity/engine.mjs';
import { evaluateContinuity, buildContinuityGateBlock } from '../continuity/evaluator.mjs';
function isNonEmptyString(value) {
@@ -29,7 +33,7 @@ export function buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanRes
const taskBoundaryStop = wrapperResult?.taskBoundaryStop === true || replyClosureState === 'completed';
const highRiskStop = wrapperResult?.highRiskStop === true;
return {
return normalizeContinuityEngineInput({
planId: wrapperResult?.planId ?? 'hook-preflight-approved-plan',
currentTask: wrapperResult?.currentTask ?? wrapperResult?.requiredNextAction ?? 'hook-preflight-task',
taskState: wrapperResult?.taskState ?? (plannerDerivedAction ? 'complete' : null),
@@ -40,7 +44,11 @@ export function buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanRes
sameApprovedPlan,
taskBoundaryStop,
highRiskStop,
};
metadata: {
adapterSource: 'force-recall',
classification: wrapperResult?.classification ?? null,
},
});
}
export function createForceRecallContinuityAdapter(config = {}) {
@@ -50,10 +58,22 @@ export function createForceRecallContinuityAdapter(config = {}) {
return {
evaluate({ wrapperResult, autoChainPlanResult = null }) {
const input = buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanResult);
if (!input) return { input: null, result: null, block: '' };
const result = evaluateContinuity(input, { legalTerminalStates });
const block = buildContinuityGateBlock(result, { legalTerminalStates, label });
return { input, result, block };
if (!input) {
return createContinuityEngineContract({
input: null,
evaluation: null,
block: '',
options: { adapterName: 'force-recall', label },
});
}
const evaluation = evaluateContinuity(input, { legalTerminalStates });
const block = buildContinuityGateBlock(evaluation, { legalTerminalStates, label });
return createContinuityEngineContract({
input,
evaluation,
block,
options: { adapterName: 'force-recall', label },
});
},
};
}

View File

@@ -0,0 +1,98 @@
import {
normalizeContinuityEngineInput,
createContinuityEngineContract,
} from '../continuity/engine.mjs';
import { evaluateContinuity, buildContinuityGateBlock } from '../continuity/evaluator.mjs';
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
export function buildGenericContinuityInput(source = {}) {
const normalized = normalizeContinuityEngineInput(source);
if (!normalized.planId || !normalized.currentTask) {
return null;
}
return normalized;
}
export function createGenericPreflightContinuityAdapter(config = {}) {
const legalTerminalStates = config?.legalTerminalStates;
const label = config?.adapter?.genericPreflight?.injectBlockLabel
?? config?.adapter?.forceRecall?.injectBlockLabel
?? 'APPROVED_PLAN_CONTINUITY_GATE';
return {
evaluate(source = {}) {
const input = buildGenericContinuityInput(source);
if (!input) {
return createContinuityEngineContract({
input: null,
evaluation: null,
block: '',
options: { adapterName: 'generic-preflight', label },
});
}
const evaluation = evaluateContinuity(input, { legalTerminalStates });
const block = buildContinuityGateBlock(evaluation, { legalTerminalStates, label });
return createContinuityEngineContract({
input,
evaluation,
block,
options: { adapterName: 'generic-preflight', label },
});
},
};
}
export function runGenericPreflightContinuityAdapter({ source = {}, config = {} } = {}) {
return createGenericPreflightContinuityAdapter(config).evaluate(source);
}
export function runManualContinuityPreflight({
config = {},
planId,
currentTask,
taskState = null,
nextDerivedAction = null,
derivedAction = null,
replyClosureState = null,
dispatchReceipt = null,
nextTaskKnown = false,
sameApprovedPlan = false,
taskBoundaryStop = false,
highRiskStop = false,
nextTaskId = null,
nextTaskKey = null,
metadata = {},
} = {}) {
return runGenericPreflightContinuityAdapter({
config,
source: {
planId,
currentTask,
taskState,
nextDerivedAction: nextDerivedAction ?? derivedAction,
derivedAction: derivedAction ?? nextDerivedAction,
replyClosureState,
dispatchReceipt,
nextTaskKnown,
sameApprovedPlan,
taskBoundaryStop,
highRiskStop,
nextTaskId,
nextTaskKey,
metadata,
},
});
}
export default {
buildGenericContinuityInput,
createGenericPreflightContinuityAdapter,
runGenericPreflightContinuityAdapter,
runManualContinuityPreflight,
};

View File

@@ -11,6 +11,10 @@ export const defaultConfig = Object.freeze({
enabled: true,
injectBlockLabel: 'APPROVED_PLAN_CONTINUITY_GATE',
},
genericPreflight: {
enabled: true,
injectBlockLabel: 'APPROVED_PLAN_CONTINUITY_GATE',
},
},
});

View File

@@ -13,6 +13,10 @@ export const continuityConfigSchema = Object.freeze({
enabled: 'boolean',
injectBlockLabel: 'string',
},
genericPreflight: {
enabled: 'boolean',
injectBlockLabel: 'string',
},
},
});
@@ -27,8 +31,8 @@ const TOP_LEVEL_KEYS = new Set([
'adapter',
]);
const ADAPTER_KEYS = new Set(['forceRecall']);
const FORCE_RECALL_KEYS = new Set(['enabled', 'injectBlockLabel']);
const ADAPTER_KEYS = new Set(['forceRecall', 'genericPreflight']);
const ADAPTER_CONFIG_KEYS = new Set(['enabled', 'injectBlockLabel']);
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
@@ -59,6 +63,35 @@ function validateStringArray(errors, value, fieldName) {
});
}
function normalizeAdapterConfig(baseAdapterConfig, inputAdapterConfig) {
return {
...baseAdapterConfig,
...(isPlainObject(inputAdapterConfig) ? inputAdapterConfig : {}),
injectBlockLabel: typeof inputAdapterConfig?.injectBlockLabel === 'string'
? inputAdapterConfig.injectBlockLabel.trim()
: baseAdapterConfig.injectBlockLabel,
};
}
function validateNamedAdapter(errors, adapterInput, adapterKey) {
if (!(adapterKey in adapterInput)) return;
if (!isPlainObject(adapterInput[adapterKey])) {
errors.push(`adapter.${adapterKey}: expected object`);
return;
}
pushUnknownKeyErrors(errors, adapterInput[adapterKey], ADAPTER_CONFIG_KEYS, `adapter.${adapterKey}.`);
if ('enabled' in adapterInput[adapterKey] && typeof adapterInput[adapterKey].enabled !== 'boolean') {
errors.push(`adapter.${adapterKey}.enabled: expected boolean`);
}
if ('injectBlockLabel' in adapterInput[adapterKey] && !isNonEmptyString(adapterInput[adapterKey].injectBlockLabel)) {
errors.push(`adapter.${adapterKey}.injectBlockLabel: expected non-empty string`);
}
}
export function normalizeContinuityConfig(input = {}) {
const base = cloneDefaultConfig();
@@ -79,17 +112,11 @@ export function normalizeContinuityConfig(input = {}) {
adapter: {
...base.adapter,
...(isPlainObject(input.adapter) ? input.adapter : {}),
forceRecall: {
...base.adapter.forceRecall,
...(isPlainObject(input.adapter?.forceRecall) ? input.adapter.forceRecall : {}),
},
forceRecall: normalizeAdapterConfig(base.adapter.forceRecall, input.adapter?.forceRecall),
genericPreflight: normalizeAdapterConfig(base.adapter.genericPreflight, input.adapter?.genericPreflight),
},
};
if (typeof normalized.adapter.forceRecall.injectBlockLabel === 'string') {
normalized.adapter.forceRecall.injectBlockLabel = normalized.adapter.forceRecall.injectBlockLabel.trim();
}
return normalized;
}
@@ -140,25 +167,8 @@ export function validateContinuityConfig(input = {}) {
errors.push('adapter: expected object');
} else {
pushUnknownKeyErrors(errors, input.adapter, ADAPTER_KEYS, 'adapter.');
if ('forceRecall' in input.adapter) {
if (!isPlainObject(input.adapter.forceRecall)) {
errors.push('adapter.forceRecall: expected object');
} else {
pushUnknownKeyErrors(errors, input.adapter.forceRecall, FORCE_RECALL_KEYS, 'adapter.forceRecall.');
if ('enabled' in input.adapter.forceRecall && typeof input.adapter.forceRecall.enabled !== 'boolean') {
errors.push('adapter.forceRecall.enabled: expected boolean');
}
if (
'injectBlockLabel' in input.adapter.forceRecall
&& !isNonEmptyString(input.adapter.forceRecall.injectBlockLabel)
) {
errors.push('adapter.forceRecall.injectBlockLabel: expected non-empty string');
}
}
}
validateNamedAdapter(errors, input.adapter, 'forceRecall');
validateNamedAdapter(errors, input.adapter, 'genericPreflight');
}
}

View File

@@ -0,0 +1,66 @@
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
export function normalizeContinuityEngineInput(input = {}) {
if (!isPlainObject(input)) return {};
const normalized = {
planId: isNonEmptyString(input.planId) ? input.planId.trim() : input.planId ?? null,
currentTask: isNonEmptyString(input.currentTask) ? input.currentTask.trim() : input.currentTask ?? null,
taskState: isNonEmptyString(input.taskState) ? input.taskState.trim() : input.taskState ?? null,
nextTaskId: isNonEmptyString(input.nextTaskId) ? input.nextTaskId.trim() : input.nextTaskId ?? null,
nextTaskKey: isNonEmptyString(input.nextTaskKey) ? input.nextTaskKey.trim() : input.nextTaskKey ?? null,
nextDerivedAction: input.nextDerivedAction ?? input.derivedAction ?? null,
derivedAction: input.derivedAction ?? input.nextDerivedAction ?? null,
replyClosureState: isNonEmptyString(input.replyClosureState) ? input.replyClosureState.trim() : input.replyClosureState ?? null,
dispatchReceipt: input.dispatchReceipt ?? null,
nextTaskKnown: input.nextTaskKnown === true,
sameApprovedPlan: input.sameApprovedPlan === true,
taskBoundaryStop: input.taskBoundaryStop === true,
highRiskStop: input.highRiskStop === true,
metadata: isPlainObject(input.metadata) ? { ...input.metadata } : {},
};
return normalized;
}
export function createContinuityEngineResult(input, evaluation, options = {}) {
const label = isNonEmptyString(options.label) ? options.label.trim() : 'APPROVED_PLAN_CONTINUITY_GATE';
return {
input,
evaluation,
label,
ok: evaluation?.ok === true,
status: evaluation?.status ?? 'pass',
verdict: evaluation?.verdict ?? 'pass',
reason: evaluation?.reason ?? null,
};
}
export function createContinuityEngineContract({ input, evaluation, block, options = {} }) {
const result = createContinuityEngineResult(input, evaluation, options);
return {
input,
result,
evaluation,
block: typeof block === 'string' ? block : '',
meta: {
adapterName: options.adapterName ?? 'unknown',
label: result.label,
hostAgnostic: true,
},
};
}
export default {
normalizeContinuityEngineInput,
createContinuityEngineResult,
createContinuityEngineContract,
};

View File

@@ -1,4 +1,41 @@
# Continuity Types (MVP)
# Continuity Types
## Host-agnostic continuity engine input
The generalized engine operates on a host-agnostic input object. Adapters are responsible for mapping host or hook specific context into this shape.
Minimum practical fields:
- `planId`: string
- `currentTask`: string
- `taskState`: string | null
- `nextDerivedAction`: object | null
- `replyClosureState`: string | null
- `dispatchReceipt`: object | null
- `nextTaskKnown`: boolean
- `sameApprovedPlan`: boolean
- `taskBoundaryStop`: boolean
- `highRiskStop`: boolean
Optional fields:
- `nextTaskId`: string | null
- `nextTaskKey`: string | null
- `derivedAction`: object | null
- `metadata`: object
Normalization entrypoint:
- `normalizeContinuityEngineInput(input)`
Engine contract returned by generalized adapters:
- `input`: normalized engine input or `null`
- `result`: summarized engine result object
- `evaluation`: raw evaluator result or `null`
- `block`: injected prompt block string
- `meta.adapterName`: adapter identifier
- `meta.hostAgnostic`: always `true`
## Receipt contract
@@ -20,4 +57,6 @@ The MVP receipt validator contract uses this minimum shape:
## Notes
- This contract is intentionally minimal and keeps file I/O separate.
- It mirrors the current approved-plan dispatch receipt fields used by the existing continuity scripts.
- The engine is host-agnostic; host-specific behavior belongs in adapters.
- `force-recall` remains the parity adapter for the current hook path.
- `generic-preflight` is the minimal generalized adapter/runner for non-`force-recall` integration.

View File

@@ -4,6 +4,11 @@ import {
validateContinuityConfig,
normalizeContinuityConfig,
} from './config/schema.mjs';
import {
normalizeContinuityEngineInput,
createContinuityEngineResult,
createContinuityEngineContract,
} from './continuity/engine.mjs';
import {
evaluateContinuity,
buildContinuityGateBlock,
@@ -24,6 +29,12 @@ import {
createForceRecallContinuityAdapter,
runForceRecallContinuityAdapter,
} from './adapters/force-recall.mjs';
import {
buildGenericContinuityInput,
createGenericPreflightContinuityAdapter,
runGenericPreflightContinuityAdapter,
runManualContinuityPreflight,
} from './adapters/generic-preflight.mjs';
export {
defaultConfig,
@@ -31,6 +42,9 @@ export {
continuityConfigSchema,
validateContinuityConfig,
normalizeContinuityConfig,
normalizeContinuityEngineInput,
createContinuityEngineResult,
createContinuityEngineContract,
evaluateContinuity,
buildContinuityGateBlock,
hasValidDispatchReceipt,
@@ -43,6 +57,10 @@ export {
buildApprovedPlanContinuityInput,
createForceRecallContinuityAdapter,
runForceRecallContinuityAdapter,
buildGenericContinuityInput,
createGenericPreflightContinuityAdapter,
runGenericPreflightContinuityAdapter,
runManualContinuityPreflight,
};
export default {
@@ -51,6 +69,9 @@ export default {
continuityConfigSchema,
validateContinuityConfig,
normalizeContinuityConfig,
normalizeContinuityEngineInput,
createContinuityEngineResult,
createContinuityEngineContract,
evaluateContinuity,
buildContinuityGateBlock,
hasValidDispatchReceipt,
@@ -63,4 +84,8 @@ export default {
buildApprovedPlanContinuityInput,
createForceRecallContinuityAdapter,
runForceRecallContinuityAdapter,
buildGenericContinuityInput,
createGenericPreflightContinuityAdapter,
runGenericPreflightContinuityAdapter,
runManualContinuityPreflight,
};