Module:
moralstack/runtime/orchestrator.py(facade);moralstack/orchestration/controller.py(thin controller).
The Orchestrator is the central component of MoralStack that coordinates the entire deliberative processing flow. The runtime facade delegates to OrchestrationController, which uses injected PersistencePort (default: NullPersistence), PathRouter, OverlayPolicyApplier (overlay_policy), TraceLifecycle, and DecisionLogger for request-scoped persistence, routing, overlay sensitivity/risk floor, trace lifecycle, and decision explanation logging.
For testers and stakeholders: Every response produced by the system has an explicit final action (
final_action:
NORMAL_COMPLETE | SAFE_COMPLETE | REFUSE) and a path (FAST_PATH | DELIBERATIVE_PATH), exposed in
response.metadata. These fields are the reference for benchmarks, decision correctness metrics and audit.
The Orchestrator handles:
- Request reception and preprocessing
- Developer Contract Compliance Layer (DCCL) evaluation and optional compliance fast-path
- Initial risk estimation (parallel with speculative generation when enabled)
- Routing between Fast Path, Compliance Fast Path, and Deliberative Path
- Deliberative cycle coordination
- Guidance aggregation from modules
- Final response assembly
Conversation linkage (optional, foundation only): OrchestrationController.process / Orchestrator.process accept
optional keyword-only arguments conversation_id, turn_index, parent_request_id, and conversation_state
(ConversationGovernanceState). When provided, metadata is persisted on the request row and echoed on
OrchestratorResult; they do not change routing or decide_action when omitted. See moralstack/orchestration/conversation_state.py.
Per-call context under concurrency: OrchestrationController.process() constructs a stack-local ProcessCallContext (moralstack/orchestration/process_context.py) and passes it to internal helpers (_apply_conversation_metadata_to_result, ledger follow-up, canonical conversation events). A single controller instance may handle overlapping process() calls (for example from the HTTP proxy threadpool); conversation linkage and ledger intent scratch fields must never live on self for that reason.
When OrchestrationController is constructed with a non-None SemanticDecisionLedger and process(..., conversation_id=...)
is used, the controller consults the ledger after decide_action and initial routing. On a semantic cache hit,
ConversationalFastPathRunner (moralstack/orchestration/conversational_fast_path.py) may patch Decision and the
string route to skip deliberation only when a conservative gate allows (cached REFUSE, or current route already
non-deliberative). Response text is never read from the ledger (DAF-4); only governance metadata is reused.
moralstack/orchestration/system_prompt_resolver.py exposes effective_system_for_request(...), composing the policy system prompt per request from the protected base, optional non-empty DeveloperContract.raw_text, and an optional mode suffix (normal, safe_complete, constrained). When no contract text is present, output matches the legacy single-turn byte strings. The suffix modes remain available for other call sites; DeliberationRunner does not use safe_complete or constrained resolver modes for policy generation (see below).
Governance prompt placement (v0.4 Step 10):
- Internal policy (
DeliberationRunner):SAFE_COMPLETE_GENERATION_INSTRUCTIONandCONSTRAINED_GENERATION_INSTRUCTIONfrommoralstack/orchestration/_policy_helpers.pyare prefixed onto the user-facing prompt passed topolicy.generate/policy.rewrite. The system string passed to the policy LLM is still composed witheffective_system_for_request(..., mode="normal")(contract overlay only, no governance suffix from those constants). - Python SDK (
moralstack/sdk/wrapper.py): whenfinal_action == "SAFE_COMPLETE", governance guidance is sent as an extra trailing message withrole="user"appended tomessages. Existing system message content is not modified.
Refusal context (moralstack/orchestration/refusal_context.py):
build_refusal_context(...)produces a frozenRefusalContextfor refusal LLM calls. Optional keyword argumentsdeveloper_contractandconversation_historypopulate bounded snippetsdeveloper_contract_summaryandconversation_history_snippet.classify_refusal_focusimplements priority order P0–P6 (documented in the source docstring): P0 hard topical signals are never overridden by deployer contract hints; P1 applies only whenDeveloperContract.mode == "structured"and redirection keywords appear inraw_text; domain overlay redirection continues to integrate viarefusal_redirectionas before.
In v0.4 foundations, ConversationGovernanceState includes additive fields for future multi-turn routing:
last_developer_contract_hash, last_governance_posture, and turn_decisions_summary (tuple of
TurnDecisionSummary). should_full_refresh(*, current_turn: TurnContext | None = None) preserves legacy behavior
(should_full_refresh() without arguments remains conservative and returns True) and enables forward-compatible
rule evaluation when current_turn is provided.
┌─────────────────────────────────────────────────────────────────┐
│ ORCHESTRATOR │
│ │
│ Request ──► Risk Estimation ──► Path Selection │
│ │ │
│ ┌────────────────────────┴────────────────────┐ │
│ ▼ ▼ │
│ FAST PATH DELIBERATIVE PATH │
│ (risk < 0.3) (risk ≥ 0.3) │
│ │ │ │
│ ▼ ▼ │
│ Direct Generation ┌──────────────────────┐ │
│ │ │ DELIBERATIVE CYCLE │ │
│ │ │ ┌──────────────────┐ │ │
│ │ │ │ Generate/Revise │ │ │
│ │ │ │ Critic │ │ │
│ │ │ │ Simulator │ │ │
│ │ │ │ Perspectives │ │ │
│ │ │ │ Hindsight │ │ │
│ │ │ │ Convergence │ │ │
│ │ │ └──────────────────┘ │ │
│ │ └──────────────────────┘ │
│ └─────────────────────────────┬─────────────────────┘
│ ▼ │
│ Response Assembly │
└─────────────────────────────────────────────────────────────────┘
The deliberation runner and controller consume results from cognitive modules (critic, simulator, hindsight, perspectives) and from the policy LLM via Protocol types defined in moralstack/orchestration/types.py. These protocols provide a typed contract (structural subtyping) so that:
- Concrete module return types (e.g.
CriticReport,SimulationResult,HindsightResult,EnsembleResult) satisfy the protocols without orchestration importing the runtime modules. - Refactors that rename or change attributes are caught by type checkers where the runner uses direct attribute access.
Protocols include: PolicyGenerationResultProtocol, CriticReportProtocol, QuickCheckResultProtocol, SimulationResultProtocol, HindsightResultProtocol, PerspectiveResultProtocol, EnsembleResultProtocol, RiskEstimationProtocol, LoggerProtocol, ConstitutionStoreProtocol, and OutputProtectorProtocol. DeliberationDependencies (injected into DeliberationRunner) is a dataclass whose fields are typed with the corresponding protocols (PolicyLLMProtocol, CriticProtocol, SimulatorProtocol, HindsightProtocol, PerspectiveEnsembleProtocol, ConstitutionStoreProtocol, OutputProtectorProtocol) from moralstack/core/types.py and moralstack/orchestration/types.py. DeliberationState fields (critiques, simulations, hindsight, perspectives, _perspectives_aggregation) are typed with these protocols. The package moralstack.orchestration is checked with mypy strict (see pyproject.toml); some runtime module APIs may extend the base protocols with optional keyword arguments.
DeliberationRunner.run_fast_path and DeliberationRunner.run_deliberative_path accept an optional keyword argument constitution. When the controller passes a pre-loaded constitution (one load per request via get_constitution_safe), the runner reuses it for quick_check, assemble, and critique, avoiding multiple store lookups. If constitution is omitted, the runner loads from the store when needed (backward compatible).
DeliberationRunner delegates to dedicated modules to keep responsibilities separated:
- guidance_builder — Builds aggregated guidance string from critic, perspectives, hindsight, and simulator state (
build_aggregated_guidance(state, *, filter_marginal=True, telemetry=None)). Applies signal-strength filtering by default (filter_marginal=True): critic guidance is included only when violations exist or critic decision isREVISE/REFUSE; perspective suggestions only from dissatisfied perspectives (approval_score< 0.75) and only when weighted approval < 0.85; hindsight only when a real hindsight signal exists andhindsight_score< 0.7; simulator only whensemantic_expected_harm≥ 0.35. Hard violations bypass all filters. When all modules are satisfied, guidance returns empty and the rewrite is skipped (draft from the previous cycle is preserved). Emitsrewrite (SKIPPED_EMPTY_GUIDANCE)in persisted LLM call rows when the rewrite is skipped; logsguidance_filter:lines at INFO. Also emitsAGGREGATED_GUIDANCE_EVALUATEDorchestration events for the request detail UI. - convergence_evaluator — Evaluates whether the deliberation has converged and which
DecisionTypeto apply (ConvergenceEvaluator(config).determine_decision(state, risk_estimation=None)). Cycle 1 may stop early only via a conservative_evaluate_cycle1_early_convergencegate (stricter than the legacy cycle>=2 weighted-perspectives path); rejection is explicit and observable. Invariants and structured logging for the loop remain in convergence.py (enforce_convergence_invariants,log_convergence_event). Observability:CONVERGENCE_EVALUATED,EARLY_CONVERGENCE_ACCEPTED,EARLY_CONVERGENCE_REJECTED;CYCLE_SUMMARYincludesearly_convergence_considered,early_convergence_accepted,convergence_reason_codes,deliberation_decision. - language_resolver — Resolves explicit language and builds prompt with language prefix (
resolve_prompt_with_language(prompt, detected_iso, fallback_prompt)), reusing logic fromsafe_refusal_generatorand_policy_helpers. - persistence_helpers — Centralizes optional diagnostics logging and LLM call persistence (
record_llm_call(logger, diagnostics_payload, persist_kwargs)).
DeliberationRunner._apply_hindsight_if_needed and _evaluate_hindsight emit structured events via orch_debug_log with hypothesisId H-hindsight-path and payload component: hindsight_diagnostic. The outcome field records the branch taken, for example: disabled_by_config, skipped_no_module, gated_not_final_cycle, invoke_evaluate, evaluate_ok, evaluate_aborted_timeout_guard, evaluate_failed, evaluate_failed_orchestrator_timeout.
Observability routing follows MORALSTACK_OBSERVABILITY_MODE (see moralstack/observability/config.py): db_only inserts into SQLite debug_events; dual writes both DB and logs/observability/debug.event.jsonl; file_only writes the JSONL file only. The legacy alias MORALSTACK_PERSIST_MODE is still accepted with a deprecation warning. The same events appear in the root logger at INFO as hindsight_diagnostic outcome=....
from moralstack.runtime.orchestrator import OrchestratorConfig, RiskThresholds
config = OrchestratorConfig(
max_deliberation_cycles=2, # Maximum number of deliberative cycles
timeout_ms=600000, # Total timeout (10 minutes)
risk_thresholds=RiskThresholds(
low=0.3, # Below: fast path
medium=0.7, # Between low and medium: standard deliberation
),
enable_simulation=True, # Enable Consequence Simulator
enable_hindsight=True, # Enable Hindsight Evaluator
enable_perspectives=True, # Enable Perspective Ensemble
min_hindsight_score=0.8, # Minimum score for convergence
)| Parameter | Default | Description |
|---|---|---|
max_deliberation_cycles |
2 | Maximum deliberative cycle iterations |
timeout_ms |
600000 | Total timeout in milliseconds |
risk_thresholds.low |
0.3 | Threshold for fast path |
risk_thresholds.medium |
0.7 | Threshold for extended deliberation |
min_hindsight_score |
0.8 | Minimum hindsight score for convergence |
borderline_refuse_upper |
0.95 | Upper bound for borderline REFUSE deliberation |
early_exit_perspectives_threshold |
0.85 | Weighted approval threshold for early exit (critic PROCEED path) |
cycle1_early_convergence_min_weighted_approval |
0.78 | Minimum weighted perspectives approval for cycle-1 early convergence. Perspectives with weights: vulnerable ×1.2, compliance ×1.1, adversary ×0.8, user/observer ×1.0. |
cycle1_early_convergence_max_semantic_harm |
0.35 | Maximum simulator semantic_expected_harm for cycle-1 early convergence (when simulation is enabled). Queries with harm above this threshold always get cycle 2. |
cycle1_early_convergence_min_per_perspective_approval |
0.70 | Minimum approval_score for each individual perspective. If any single perspective scores below this, cycle 2 is forced. |
parallel_critic_with_modules |
True |
Legacy static fork when enable_dynamic_parallel_scheduler is False or risk estimation is unavailable: when True and parallel_module_calls is True, critic runs in parallel with simulator and perspectives. See Latency-oriented parameters. |
enable_dynamic_parallel_scheduler |
True |
When True and parallel_module_calls is True, each cycle selects critic_gated vs full_parallel from risk posture (execution only; governance unchanged). See Latency-oriented parameters. |
enable_speculative_generation |
True |
When True, risk estimation and speculative draft generation run in parallel before routing. The draft is reused on benign, fast, and deliberative routes when applicable. See Latency-oriented parameters. |
These flags reduce wall-clock latency without changing routing policy (decide_action, get_route, overlay floors, or convergence invariants). They do not change how final_action is computed from risk and module outputs.
enable_dynamic_parallel_scheduler (default True)
- Requires
parallel_module_calls=True. When both areTrue,DeliberationRunnerchoosescritic_gated(critic first; simulator and perspectives only if no hard violation) vsfull_parallel(critic, simulator, perspectives concurrently) per cycle using existing risk fields (risk_category,operational_risk,intent_to_harm,requested_instructions,risk_policy_action, prior-cycle hard critiques). EmitsPARALLEL_STRATEGY_SELECTEDand enrichesCYCLE_SUMMARYtraces (scheduler_reason_codes,critic_short_circuit). - When
critic_gatedshort-circuits (hard violation after critic): simulator and perspectives are not invoked; emitsCRITIC_SHORT_CIRCUIT_TRIGGERED. - When
False: the runner uses the legacyparallel_critic_with_modulesboolean alone to choose the fork (same behavior as before dynamic scheduling).
parallel_critic_with_modules (default True)
- Requires
parallel_module_calls=True. When both areTrue, each deliberation cycle runs critic, simulator, and perspectives concurrently (three parallel LLM calls per cycle when those modules are enabled). - When
False: the runner uses a two-stage layout: critic runs first; only if there is no hard violation do simulator and perspectives run in parallel. This avoids paying for simulator/perspective calls when the critic would reject the draft, but adds sequential critic latency before sim/persp start. - Hard violations: If the critic reports a hard violation, simulator and perspective results from that cycle are discarded and do not affect merged state. Convergence and refusal logic see the same critic outcome as in the gated layout; you may pay extra token cost in the rare hard-violation case.
- Set to
falseif you prioritize minimizing LLM spend on hard-violation paths over latency.
enable_speculative_generation (default True)
- When
True:OrchestrationControllerstarts risk estimation and a speculative policygenerate(same base system prompt as normal first-pass generation) in parallel. After risk returns, routing proceeds as usual; the speculative draft is not used for policy routing decisions. - Reuse: On benign fast path, fast path, and deliberative path, the draft is reused when it is still valid (skipping a duplicate first
generatewhere implemented). On REFUSE, the speculative call is unused (wasted latency/token trade-off).SAFE_COMPLETEpath does not reuse this draft (separate policy call with SAFE_COMPLETE governance prefixed on the user prompt). - Constrained generation (
CLEARLY_HARMFULdeliberation): the speculative draft is not applied as cycle-1 output; constrained governance text is supplied as a prompt prefix (not viamode="constrained"on the system resolver). - Note: Speculative generation uses language resolution before the risk estimator’s
detected_languageis available (fallback path). Routing and safety decisions are unchanged; draft wording may differ slightly from a strictly sequential generate-after-risk for the same request.
enable_simulator_gating (default false)
- Requires
enable_simulation=Trueand an available simulator module. Whentrue, each cycle evaluates a conservative simulator gate (see env varsMORALSTACK_ORCHESTRATOR_SIMULATOR_GATE_*). Cycle 1 always runs the simulator; cycle 2+ may skip only when prior harm is very low, the draft has not materially changed, risk posture is not elevated, and (when the critic has already run) the critic is clean. Full parallel scheduling uses a stricter prior-only check before the critic runs because the critic is not visible to the gate yet. - Observability:
SIMULATOR_GATE_DECISION,SIMULATOR_EXECUTED,SIMULATOR_SKIPPED;CYCLE_SUMMARYincludessimulator_ran_this_cycle,simulator_gate_reason_codes, andsimulator_carry_forward.
MoralStack supports a configurable parameter borderline_refuse_upper that defines the inclusive upper bound
for invoking the deliberative path on REFUSE decisions.
A REFUSE with risk_score in the interval:
risk_thresholds.medium <= risk_score <= borderline_refuse_upper
will enter the deliberative pipeline instead of being handled with early-fast refusal.
This parameter is useful when you want more contextual reasoning for borderline cases that are not extreme.
- Default:
0.95 - Type:
floatbetween0.0and1.0
Configuration example:
OrchestratorConfig(
risk_thresholds=RiskThresholds(low=0.3, medium=0.7),
borderline_refuse_upper=0.90, # more restrictive: deliberate up to 0.90
)When risk_score > borderline_refuse_upper, the system applies early-fast refusal as before.
Path and trace after borderline deliberation: When REFUSE is in the borderline range and the
deliberative loop is run, the execution path is DELIBERATIVE_PATH. The FINAL decision trace and
response.metadata.path are set to DELIBERATIVE_PATH (not FAST_PATH), and total_cycles in
the report reflect the number of deliberative cycles actually executed.
See also: documentation on risk_thresholds.
When a constitutional overlay is marked sensitive: true, the Controller applies a floor to risk_score:
OVERLAY_SENSITIVE_RISK_FLOOR = 0.35(constant inmoralstack/orchestration/overlay_policy.py, imported by controller)- If
overlay.sensitive == Trueandrisk_score < 0.35, thenrisk_score = 0.35 - This forces routing toward the deliberative path for all sensitive domains, even when the risk estimator assigns a low score
For low-risk requests, the system uses an optimized path:
Request → Risk Estimation (< 0.3) → Direct Generation → Response
Typical latency: ~10-12s (benchmark, 84 questions)
For significant-risk requests:
- Generation/Revision: Generate or revise the draft
- Constitutional Critique: Check constitutional violations
- Consequence Simulation: Simulate future scenarios
- Perspective Ensemble: Evaluate from multiple perspectives
- Hindsight Evaluation: Retrospective evaluation
- Convergence Check: Verify termination criteria
Typical latency (84-question benchmark, run 12): overall deliberative workload mean ~36s, median ~26s; SAFE_COMPLETE one-cycle subset ~23s mean; two-cycle SAFE_COMPLETE is higher per query. Sensitive-domain queries often sit at the upper end of the range. Fast path averages ~12s (~37% of queries, including REFUSE).
The deliberative cycle terminates when:
- Max cycles reached: Maximum number of iterations
- Early exit (critic PROCEED + perspectives): Critic has zero violations and weighted perspectives approval ≥
early_exit_perspectives_threshold(default 0.85). Does not require hindsight (which is skipped in cycle 1 whenenable_hindsight_gating=True). ProducesCONVERGEDorCONVERGED_WITH_SUGGESTIONSif pending suggestions exist. - Satisfactory scores:
hindsight_score ≥ 0.8with no critical violations - All modules satisfied: Critic without violations, Perspectives with high approval, Hindsight with "proceed"
- Stable quality: After 2+ cycles with high and stable scores
In phase logs and reports, "Cycles exhausted" is displayed only when state.cycle >= max_cycles (display-only). It
is not derived from the internal convergence heuristic. Thus "Decision: converged" and "Cycles exhausted: True" do not
appear together when the run converged before reaching the cycle limit.
If deliberation exhausts cycles (stop_reason == "CYCLES_EXHAUSTED") and the post-deliberation decision is
NORMAL_COMPLETE, the Controller checks whether the context is sensitive:
risk_category ∈ {SENSITIVE, MORALLY_NUANCED}or overlaysensitive: true
If so, the decision is forced to SAFE_COMPLETE with reason code
cycles_exhausted_sensitive_fallback. This fallback never degrades a REFUSE.
The Orchestrator aggregates feedback from all modules to guide revisions:
# Aggregated guidance sources:
# 1. Critic → revision_guidance (violations and suggestions)
# 2. Perspectives → suggestions and concerns
# 3. Hindsight → feedback and suggestions
# 4. Simulator → negative consequences (if valence < 0)The aggregated guidance is passed to the Policy for revision:
[CRITIC] Add medical disclaimer
[PERSPECTIVES - Suggestions] Vulnerable User: Acknowledge emotional impact
[PERSPECTIVES - Concerns] Vulnerable User: Tone too detached
[HINDSIGHT] Low score (0.45). Improve ethical balance
[SIMULATOR] Negative consequence: User might ignore warning signs
The Orchestrator exposes a deterministic final action, independent of the generated text:
| final_action | Meaning | Typical response_type |
|---|---|---|
| NORMAL_COMPLETE | Authorized, direct response | DIRECT |
| SAFE_COMPLETE | Response with responsible framing and explicit limits | WITH_CAVEAT |
| REFUSE | Refusal with explanation and redirect | FULL_REFUSAL, PARTIAL_REFUSAL, REDIRECT |
The decision is made by decide_action() based on risk signals (constitution, hindsight, simulation,
perspectives) and is not overwritten during assembly.
| Type | Description |
|---|---|
DIRECT |
Direct response without modifications |
WITH_CAVEAT |
Response with disclaimer/warnings |
PARTIAL_REFUSAL |
Refuse part, respond to rest |
FULL_REFUSAL |
Full refusal with explanation |
REDIRECT |
Suggest alternatives |
final_action == "NORMAL_COMPLETE"→response_type == DIRECTfinal_action == "SAFE_COMPLETE"→response_type == WITH_CAVEATfinal_action == "REFUSE"→response_typein (FULL_REFUSAL,PARTIAL_REFUSAL,REDIRECT)
from moralstack.runtime.orchestrator import Orchestrator, OrchestratorConfig
# Initialize with all modules
orchestrator = Orchestrator(
config=config,
policy=policy_llm,
risk_estimator=risk_estimator,
critic=critic,
simulator=simulator,
hindsight=hindsight,
perspectives=perspectives,
constitution_store=constitution_store,
)
# Process request
result = orchestrator.process("How can I manage stress?")
# Access results
print(f"Type: {result.response.response_type}")
print(f"Content: {result.response.content}")
print(f"Cycles: {result.total_cycles}")
print(f"Path: {result.path_taken}")
print(f"Converged: {result.converged}")@dataclass
class OrchestratorResult:
response: FinalResponse # Final response
request_id: str # Request UUID
path_taken: str # "fast" or "deliberative"
total_cycles: int # Deliberative cycles executed
converged: bool # True if convergence reached
error: str | None # Optional error message@dataclass
class FinalResponse:
content: str # Generative output only; never critic rationale/guidance
response_type: ResponseType # Response type
metadata: ResponseMetadata # Detailed metadata
policy_overlay: PolicyOverlay | None = None # Normative structure (principle_ids, caveat_type)
meta_analysis: MetaAnalysis | None = None # For audit; never in contentInvariants: Content purity (no critic text in content); language-agnostic (no hardcoded strings).
@dataclass
class ResponseMetadata:
risk_score: float # Risk score [0, 1]
deliberation_cycles: int # Number of cycles (0 for FAST_PATH)
hindsight_score: float # Final hindsight score
triggered_principles: list[str] # Triggered principles
processing_time_ms: int # Processing time
# Policy-driven (for benchmark and audit)
final_action: str # "REFUSE" | "SAFE_COMPLETE" | "NORMAL_COMPLETE"
path: str # "FAST_PATH" | "DELIBERATIVE_PATH"
cycles: int # 0 for FAST_PATH, otherwise deliberative cycles
intent_clarity: str # LOW | MEDIUM | HIGH (semantic signals)
misuse_plausibility: str # LOW | MEDIUM | HIGH
actionability_risk: str # LOW | MEDIUM | HIGH
decision_correctness: dict[str, Any] | None # optional DCF payload from diagnosticsConstruction: Always build metadata via factory methods for consistency across paths (fast, deliberative, safe_complete, domain_excluded, system error). Use ResponseMetadata.from_decision(...) for flows that have a Decision (and optional DecisionExplanation); use ResponseMetadata.for_system_error(...), for_domain_excluded(...), or for_fail_safe(...) for timeout, domain-excluded, and FAIL_SAFE fallback respectively. See docs/architecture_spec.md (ResponseMetadata Construction) for the full list.
The deliberative loop uses an explicit state model (ConvergenceOutcome) and a central function
enforce_convergence_invariants:
- cycle ≥ max_cycles →
should_continue=False,stop_reason=CYCLES_EXHAUSTED(no "continue" when cycles exhausted). - CONVERGED decision →
should_continue=False,stop_reason=CONVERGED. - The loop exits only based on the post-enforcement outcome;
convergedin the result is that of the outcome. - Fast path (0 cycles): For runs with no deliberative cycles and
final_actionother than REFUSE, the FINAL trace is written withstop_reason=CONVERGEDso that reports and UI show converged=True consistently.
Structured log (JSON) with request_id and event:
CONVERGENCE_RAW– raw outcome before enforcement.CONVERGENCE_ENFORCED– final outcome (should_continue, stop_reason, cycle, max_cycles).CONVERGENCE_EXIT– loop exit (stop_reason, total_cycles, converged).SAFE_COMPLETE_DOWNGRADED– downgrade to NORMAL_COMPLETE (domain, reason).
In benchmark, execution_trace[request_id]["events"] may contain CONVERGENCE_ENFORCED and SAFE_COMPLETE_DOWNGRADED
for verification.
Modules: moralstack.orchestration.convergence, moralstack.orchestration.safe_complete_gating.
The Orchestrator implements a fail-safe system:
- Timeout: If it exceeds
timeout_ms, returns a safe default response - Module errors: Catches exceptions and continues with fallback values
- Parsing failures: Automatic retries with reformulated prompts
# Safe default response on error
FinalResponse.safe_default(processing_time_ms=elapsed)All orchestrator tuning can be overridden via .env. Variables are read when building the OrchestratorConfig; empty
or missing values use the defaults below. See .env.template for the full list. In application runs (CLI and
benchmark), orchestrator configuration is the single source of configuration — no CLI override.
There is no dedicated model for the orchestrator (it is not an LLM module).
- Default:
2 - Type: int (>= 1)
- Description: Maximum number of deliberative cycle iterations.
- Default:
600000 - Type: int (>= 1)
- Description: Total timeout in milliseconds (default 10 minutes).
- Default:
true - Type: bool
- Description: When true, the deliberation runner uses parallel execution for module calls within each cycle (see
also
MORALSTACK_ORCHESTRATOR_PARALLEL_CRITIC_WITH_MODULES). LLM calls are persisted (with run/request/cycle context) and appear in moralstack-ui;MORALSTACK_ORCHESTRATOR_ENABLE_PERSPECTIVES,MORALSTACK_ORCHESTRATOR_ENABLE_SIMULATION, andMORALSTACK_ORCHESTRATOR_ENABLE_HINDSIGHTdetermine which modules run and thus which calls are recorded and visible in the UI.
- Default:
true - Type: bool
- Description: When
trueandMORALSTACK_ORCHESTRATOR_PARALLEL_MODULE_CALLSistrue, each deliberation cycle selectscritic_gatedvsfull_parallelfrom existing risk posture signals (execution scheduling only). Whenfalse, the static fork uses onlyMORALSTACK_ORCHESTRATOR_PARALLEL_CRITIC_WITH_MODULES. See Latency-oriented parameters.
- Default:
true - Type: bool
- Description: Legacy static fork when
MORALSTACK_ORCHESTRATOR_ENABLE_DYNAMIC_PARALLEL_SCHEDULERisfalseor risk estimation is unavailable: whentrueandMORALSTACK_ORCHESTRATOR_PARALLEL_MODULE_CALLSistrue, the critic runs in parallel with the simulator and perspectives (full parallel evaluation). Whenfalse, the critic runs first as a gate; simulator and perspectives run in parallel only after the critic reports no hard violation. See Latency-oriented parameters.
- Default:
true - Type: bool
- Description: When
true, risk estimation and speculative first-pass draft generation run in parallel at the start ofprocess(). The draft may be reused on benign, fast, and deliberative routes; it is not used for routing. Whenfalse, risk estimation runs alone, then generation follows the previous sequential pattern. See Latency-oriented parameters.
- Default:
0.3 - Type: float (0–1)
- Description: Risk score below this takes the Fast Path.
- Default:
0.7 - Type: float (0–1)
- Description: Risk score at or above this triggers full deliberation.
- Default:
true - Type: bool
- Description: Enable the Perspective Ensemble module.
- Default:
true - Type: bool
- Description: Enable the Consequence Simulator module.
- Default:
true - Type: bool
- Description: Enable the Hindsight Evaluator module.
- Default:
3 - Type: int (>= 1)
- Description: Number of simulation scenarios per cycle.
- Default:
0.8 - Type: float (0–1)
- Description: Minimum hindsight score for the deliberative cycle to converge.
- Default:
0 - Type: int (>= 0)
- Description: Maximum critical violations allowed before forcing refusal.
- Default:
0.6 - Type: float (0–1)
- Description: Hindsight threshold for early exit from deliberation.
- Default:
true - Type: bool
- Description: When true, return a safe default response on error instead of raising.
- Default:
0.95 - Type: float (0–1)
- Description: Upper bound for borderline REFUSE deliberation. REFUSE decisions with risk_score between
risk_thresholds.mediumand this value enter the deliberative pipeline.
- Default:
0.95 - Type: float (0–1)
- Description: Risk score above which optional modules are skipped.
- Default:
0.90 - Type: float (0–1)
- Description: Fraction of timeout_ms at which soft timeout warnings are triggered.
- Default:
false - Type: bool
- Description: When
true,DeliberationRunnermay skip re-running the simulator in cycle 2+ only when conservative evidence supports it (low priorsemantic_expected_harm, stable draft, clean critic when available, low risk posture). EmitsSIMULATOR_GATE_DECISION,SIMULATOR_EXECUTED, andSIMULATOR_SKIPPEDand enrichesCYCLE_SUMMARYtraces. Whenfalse, the simulator always runs (unless disabled or unavailable).
- Default:
true - Type: bool
- Description: When true (default), skip hindsight except in the final cycle to reduce time and tokens. Set to false to run hindsight in every cycle (legacy behavior).
- Default:
0.85 - Type: float (0–1)
- Description: Weighted perspectives approval threshold for early exit when critic returns PROCEED (zero violations). When approval ≥ this value, the cycle converges without waiting for hindsight. Set to
1.0to effectively disable this early-exit path. Respects perspective weights (vulnerable×1.2,compliance×1.1,adversary×0.8). - Configuration source: This is currently a dataclass config field (
OrchestratorConfig.early_exit_perspectives_threshold) and is not loaded fromMORALSTACK_ORCHESTRATOR_*environment variables.
- Default:
0.78 - Type: float (0–1)
- Description: Minimum weighted perspectives approval for cycle-1 early convergence. When all cycle-1 gates pass
(critic clean, no hard violations, risk posture not elevated, perspectives aligned, simulator harm low), the
deliberation stops after 1 cycle. This threshold is stricter than
early_exit_perspectives_threshold(0.85) because it applies without hindsight confirmation. Set to1.0to effectively disable cycle-1 early convergence.
- Default:
0.35 - Type: float (0–1)
- Description: Maximum simulator
semantic_expected_harmallowed for cycle-1 early convergence. Queries where the simulator reports harm above this threshold always proceed to cycle 2 for additional review. Only evaluated whenenable_simulation=Trueand the simulator produced results. Set to0.0to require zero simulated harm for early convergence.
- Default:
0.70 - Type: float (0–1)
- Description: Minimum
approval_scorerequired from each individual perspective for cycle-1 early convergence. If any single perspective (user, vulnerable, observer, adversary, compliance) scores below this, cycle 2 is forced. This prevents early convergence when one perspective is dissatisfied even if the weighted average is high.
- Default:
0.4 - Type: float (0–1)
- Description: Run simulator if previous
semantic_expected_harm>= this threshold.
- Default:
100 - Type: int (>= 0)
- Description: Run simulator if draft changed by >= this many characters.
- Default:
0.25 - Type: float (0–1)
- Description: Conservative skip is allowed only when the previous cycle’s
semantic_expected_harmis strictly below this value (and other gates pass). Values in[skip_max, simulator_gate_semantic_harm_threshold)still require a simulator run (borderline band).
moralstack/orchestration/refusal_context.py—RefusalContext,build_refusal_context,classify_refusal_focus(summarized under Refusal context in Overview above)- Risk Estimator - Risk classification
- Constitutional Critic - Constitutional validation
- Consequence Simulator - Consequence simulation
- Hindsight Evaluator - Retrospective evaluation
- Perspective Ensemble - Multi-perspective analysis