Research contracts, sessions, approvals, and stopping¶
ResearchContract and AutoResearchSession are the governance foundation of NAVIER AutoResearch.
The contract defines what the campaign is allowed to do. The session records what the campaign actually proposes, receives approval for, executes through explicit adapters, finds, and decides.
Object model¶
classDiagram
class ResearchObjective {
+name: str
+prompt: str
+domain: str
+inputs: tuple
+targets: tuple
+generalization: tuple
+success_metrics: tuple
+constraints: Mapping
}
class ResearchBudget {
+max_gpu_hours: float
+max_cpu_hours: float
+max_storage_gb: float
+max_download_gb: float
+max_experiments: int
}
class StopPolicy {
+min_improvement_percent: float
+patience_iterations: int
+max_iterations: int
+stop_on_budget_exhaustion: bool
+stop_on_persistent_physics_failure: bool
}
class ResearchContract {
+mode: ResearchMode
+allowed_tools: tuple
+denied_tools: tuple
+approval_required_for: tuple
+requires_approval()
+tool_is_allowed()
}
class AutoResearchSession {
+status: CampaignStatus
+iteration: int
+usage: ResourceUsage
+plan()
+propose_action()
+approve()
+can_execute()
+record_action_result()
+add_finding()
+update_usage()
+evaluate_iteration()
}
ResearchContract *-- ResearchObjective
ResearchContract *-- ResearchBudget
ResearchContract *-- StopPolicy
AutoResearchSession *-- ResearchContract
ResearchObjective¶
The objective is the machine-readable scientific problem.
from navier_cfd import ResearchObjective
objective = ResearchObjective(
name="BubbleNet hidden velocity reconstruction",
prompt="Reconstruct gas and solids velocities from EP_G history.",
domain="gas_solid_multiphase",
inputs=("gas_volume_fraction_history",),
targets=("gas_velocity", "solids_velocity"),
generalization=("unseen_superficial_gas_velocity", "late_time"),
success_metrics=(
"rmse_physical",
"profile_cosine",
"interface_rmse",
),
constraints={
"geometry": "fixed_rectangular_2d",
"gpu_memory_gb": 12,
"no_temporal_window_leakage": True,
},
)
Required design principles¶
- The prompt describes the scientific question, not a guaranteed result.
- Inputs and targets use explicit field semantics.
- Generalization defines the real test.
- Success metrics are measurable.
- Constraints separate confirmed facts from preferences.
- Missing metadata should be recorded outside the confirmed fields.
ResearchBudget¶
Budgets are hard non-negative ceilings.
from navier_cfd import ResearchBudget
budget = ResearchBudget(
max_gpu_hours=24,
max_cpu_hours=72,
max_storage_gb=200,
max_download_gb=20,
max_experiments=12,
)
A zero value means no active limit for that resource in the current implementation. It does not grant unlimited organizational permission.
Resource accounting¶
AutoResearchSession.update_usage() records cumulative increments:
session.update_usage(
gpu_hours=2.4,
cpu_hours=1.0,
storage_gb=3.2,
downloaded_gb=0.0,
experiments=1,
)
Negative increments are rejected.
StopPolicy¶
from navier_cfd import StopPolicy
policy = StopPolicy(
min_improvement_percent=1.0,
patience_iterations=3,
max_iterations=10,
stop_on_budget_exhaustion=True,
stop_on_persistent_physics_failure=True,
)
Stop logic¶
flowchart TD
ITER[Completed iteration]
INC[Increment iteration]
IMP{Improvement below threshold?}
PHYS{Physics valid?}
BUD{Budget exhausted?}
MAX{Maximum iterations?}
PAT{Patience exceeded?}
FAIL{Persistent physics failure?}
STOP[Stop campaign]
CONT[Continue campaign]
ITER --> INC --> IMP
IMP -- yes --> PAT
IMP -- no --> PHYS
PAT --> PHYS
PHYS -- no --> FAIL
PHYS -- yes --> BUD
FAIL --> BUD
BUD -- yes --> STOP
BUD -- no --> MAX
MAX -- yes --> STOP
MAX -- no --> PAT
PAT -- yes --> STOP
PAT -- no --> FAIL
FAIL -- yes --> STOP
FAIL -- no --> CONT
evaluate_iteration() can return these stop reasons:
budget_exhausted:<resources>;maximum_iterations_reached;insufficient_improvement;persistent_physics_failure.
Multiple reasons can be recorded for one iteration.
Research modes¶
Assistant¶
mode=ResearchMode.ASSISTANT
All non-read actions require approval.
Use for:
- first-time clients;
- private data;
- expensive solvers;
- uncertain field semantics;
- destructive or irreversible workflows.
Guided¶
mode=ResearchMode.GUIDED
Read actions may proceed automatically. Write, compute, external, and destructive risks require approval by default.
Use for most laboratory campaigns.
Bounded¶
mode=ResearchMode.BOUNDED
The session may act within:
- an explicit tool allowlist;
- denied-tool restrictions;
- resource ceilings;
- stop policies;
- any remaining risk approvals.
Bounded mode is not unrestricted autonomy.
Tool permissions¶
contract = ResearchContract(
objective=objective,
budget=budget,
stop_policy=policy,
mode=ResearchMode.BOUNDED,
allowed_tools=(
"navier.inspect_dataset",
"navier.calculate_metrics",
"navier.render_figure",
),
denied_tools=(
"navier.delete_run",
"navier.overwrite_checkpoint",
),
)
A tool cannot be both allowed and denied.
tool_is_allowed() follows:
- denied tools are always blocked;
- if the allowlist is empty, tools not denied are eligible;
- if the allowlist is non-empty, only listed tools are eligible.
Eligibility is separate from approval.
Action proposals¶
proposal = session.propose_action(
name="Evaluate checkpoint on strict vgas holdout",
tool="navier.evaluate_checkpoint",
arguments={
"checkpoint": "runs/bubblenet/checkpoint-best",
"split": "test_vgas_holdout",
},
risk=ActionRisk.COMPUTE,
reason="Measure unseen-operating-condition performance",
estimated_cost={"gpu_hours": 0.2},
)
An ActionProposal records:
- unique identifier;
- descriptive name;
- tool;
- arguments;
- risk;
- scientific reason;
- estimated resource cost;
- approval requirement;
- creation time.
Approval flow¶
sequenceDiagram
participant A as Agent or Codex
participant S as AutoResearchSession
participant H as Human approver
participant E as Execution adapter
A->>S: propose_action(...)
S-->>A: ActionProposal
A->>S: can_execute(id)
S-->>A: false
A->>H: Present reason, arguments, and cost
H->>S: approve(id, actor, note)
A->>S: can_execute(id)
S-->>A: true
A->>E: Execute explicit adapter
E-->>A: Result
A->>S: record_action_result(...)
A->>S: update_usage(...)
Approval does not execute the action. It only changes whether an external executor is permitted to proceed.
Findings¶
session.add_finding(
kind="computed_result",
statement="Interface RMSE is 2.8 times bulk RMSE.",
evidence=(
"diagnostics/interface_error.json",
"figures/interface_error.pdf",
),
confidence="high",
)
Recommended finding kinds:
observed_fact;computed_result;interpretation;hypothesis;risk;negative_result;reproducibility_note.
The API accepts any string, but consistent categories improve downstream auditing.
Session workspace¶
workspace/
├── research_contract.json
├── research_plan.json
├── session_state.json
├── proposals.jsonl
├── approvals.jsonl
├── actions.jsonl
├── findings.jsonl
└── decisions.jsonl
Append-only reasoning trail¶
flowchart LR
P[Proposal]
A[Approval]
R[Action result]
F[Finding]
D[Iteration decision]
P --> A --> R --> F --> D
Failed actions remain in the record. This prevents survivorship bias in automated campaigns.
Creating and loading contracts¶
Save:
contract.save("runs/project/research_contract.json")
Load:
from navier_cfd import ResearchContract
contract = ResearchContract.load(
"runs/project/research_contract.json"
)
Create an objective from a prompt:
from navier_cfd import objective_from_prompt
objective = objective_from_prompt(
"Forecast temperature and pressure in unseen reactor geometries",
name="Reactor geometry-transfer campaign",
domain="reactive_transport",
inputs=("geometry", "inlet_conditions"),
targets=("temperature", "pressure"),
generalization=("unseen_geometry",),
)
Session API example¶
from navier_cfd import (
ActionRisk,
AutoResearchSession,
ResearchBudget,
ResearchMode,
StopPolicy,
)
session = AutoResearchSession.create(
"runs/reactor-project",
"Forecast temperature and pressure in unseen reactor geometries",
name="Reactor geometry-transfer campaign",
domain="reactive_transport",
mode=ResearchMode.GUIDED,
budget=ResearchBudget(max_gpu_hours=20, max_experiments=8),
stop_policy=StopPolicy(max_iterations=8),
inputs=("geometry", "inlet_conditions"),
targets=("temperature", "pressure"),
generalization=("unseen_geometry",),
)
plan = session.plan()
print(session.summary())
proposal = session.propose_action(
name="Run FNO baseline",
tool="navier.train_model",
risk=ActionRisk.COMPUTE,
reason="Establish a common structured baseline",
estimated_cost={"gpu_hours": 1.5},
)
session.approve(
proposal.id,
approved=True,
actor="project_owner",
)
# An explicit executor performs the run here.
session.record_action_result(
proposal.id,
success=True,
result={"manifest": "runs/fno/run_manifest.json"},
)
session.update_usage(gpu_hours=1.4, experiments=1)
decision = session.evaluate_iteration(
improvement_percent=2.3,
physics_valid=True,
)
Current persistence limitation¶
v1.1.0 persists files but does not yet implement a load_session() constructor that restores in-memory proposal and approval dictionaries from JSONL. A host should maintain the active process or reconstruct state explicitly. Persistent rehydration is a suitable follow-up feature.
Safety notes¶
- Do not place credentials in contracts or action arguments.
- Do not use approval notes as a substitute for institutional access control.
- Budget checks occur when evaluating campaign state; execution adapters should also preflight estimated cost.
- A successful action result does not imply a scientifically valid result.
- A physics failure should be recorded even when the numerical metric improves.