Skip to content

Integrate dev: execution backends, Academy multi-agent, CGFastMCP, HPC/Globus#139

Merged
keceli merged 144 commits into
mainfrom
dev
Jul 19, 2026
Merged

Integrate dev: execution backends, Academy multi-agent, CGFastMCP, HPC/Globus#139
keceli merged 144 commits into
mainfrom
dev

Conversation

@tdpham2

@tdpham2 tdpham2 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Integrates 130 commits from dev into main. This is a large integration bringing distributed execution backends, the Academy multi-agent module, the CGFastMCP framework, and HPC/Globus infrastructure into main.

Scope: ~137 files, +23.5k / −725 lines. Merge is clean and config.toml remains tracked on both branches (see Notes).

Changes by area (with the PR that introduced each)

1. Pluggable execution backend layer (src/chemgraph/execution/, ~10 files)
Backend abstraction for Parsl, EnsembleLauncher (EL), and Globus Compute, with backend selection via config.toml [execution] / env vars.
→ PRs #127, #131, #132, #133

2. Academy distributed multi-agent module (src/chemgraph/academy/, ~39 files — new module)
Distributed multi-agent screening, federated cross-HPC campaigns, HTTP exchange registration, operator-console dashboard, per-agent allowed_tools whitelist, and wakeups routed through the chemgraph turn primitive. Includes a large tests/test_academy_*.py suite.
→ Landed directly on dev

3. CGFastMCP framework + MCP migration (src/chemgraph/mcp/, ~12 files)
CGFastMCP backend framework (tracker kwargs, pre-submit hook, schema_fanout_tool); migrated the XANES and gRASPA MCP servers; MACE MCP transport and persistence.
→ PRs #134, #120

4. Globus Transfer + job persistence
GlobusTransfer manager and MCP file-staging tools, inline-structure transport gated to no-shared-filesystem backends, JobTracker persistence with Globus task-UUID round-trip, and Globus Compute executor recovery.
→ PRs #120, #127

5. HPC config & Parsl hardening (src/chemgraph/hpc_configs/, parsl_tools.py)
Multi-node-safe MPI flavour per HPC system, configurable Parsl worker_init, Aurora worker parameterization, clean DFK shutdown, and serialized MACE model loads.
→ PRs #132, #133

6. Agent / model refactors
Single-agent workflows routed through run_turn (agent/turn.py), dashboard event callbacks extracted to agent/events.py, shared LLM endpoint settings (models/settings.py, models/loader.py, models/openai.py), and version read from the chemgraphagent distribution.

7. Repo hygiene
Removed the dead single_agent_architector workflow, dropped machine-specific Crux tests, and gitignored runs/ and **/*.model. config.toml stays tracked.

8. Remove evaluation dataset
Move evaluation dataset from the current repo to HF: https://huggingface.co/datasets/Autonomous-Scientific-Agents/groundtruth

Notes on config.toml

config.toml was added on main and was missing from dev (still gitignored there). A prep commit on dev restores config.toml from main's copy and removes the config.toml gitignore entry, so the file stays version-controlled on both branches and after merge.

Rolled-up PRs

#120, #127, #131, #132, #133, #134

tdpham2 and others added 30 commits May 7, 2026 16:20
…us Compute

Introduce a unified execution module with an abstract ExecutionBackend
interface and TaskSpec model, supporting four backends: local
(ProcessPoolExecutor), Parsl, EnsembleLauncher, and Globus Compute.

Includes config factory with resolution order (args > env > config.toml),
HPC configs loader, comprehensive tests, and pytest --run-globus-compute
option for live endpoint tests.
Remove dead num_nodes=1 after raise in aurora_parsl.py and fix
misleading error message. Set _initialized=False at start of
EnsembleLauncherBackend.shutdown() to prevent submitting to a
partially torn-down backend.
When backend=globus_compute, MCP tools now return immediately after
submitting jobs to the remote HPC endpoint instead of blocking until
completion. A new JobTracker tracks submitted futures across tool calls,
and new MCP tools (check_job_status, get_job_results, list_jobs,
cancel_job) let the LLM agent poll for progress and retrieve results.

Non-Globus backends (local, Parsl, EnsembleLauncher) are unchanged and
continue to block until results are ready.

Key changes:
- Add is_async_remote property to ExecutionBackend (True for Globus)
- Add check_endpoint_status() health check to GlobusComputeBackend
- Add JobTracker with batch registration, status, results, cleanup
- Add submit_or_gather() utility that branches on backend type
- Add optional timeout parameter to gather_futures()
- Add register_job_tools() to wire job tools into any MCP server
- Integrate tracker into MACE, XANES, and gRASPA MCP servers
- Add CGFastMCP: FastMCP subclass with integrated execution backend,
  lazy init, built-in job tools, @tool() and @ensemble_tool() decorators
- Refactor EnsembleLauncherBackend with client-only mode (shared
  orchestrator via checkpoint_dir) and managed mode
- Update get_backend() to route client_only vs managed EL initialization
- Rewrite mace_mcp_hpc.py to use CGFastMCP decorators
- Clean up parsl_tools.py: remove dead code, use stdlib logging
- Fix __main__ pickle issue via _fix_module_for_pickle + sys.modules alias
- Add client-only mode demo cell to notebook 3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Modified the EL backend implemenations, and added a EL backend test
…mport

- parsl_tools.run_mace_core: stop swallowing exceptions and returning None.
  run_ase_core already returns a structured failure dict on simulation
  errors, and programmer errors should propagate.
- cg_fastmcp.ensemble_tool: raise TypeError with a clear message when the
  decorated function does not have exactly one parameter, instead of
  crashing with IndexError at decoration time.
- ensemble_launcher_backend: soft-import ensemble_launcher and defer the
  failure to construction / call time. SYSTEM_CONFIG_REGISTRY is now a
  lazy view backed by builder functions so the module loads cleanly
  without EL installed, restoring the deferred-error behaviour callers
  of chemgraph.execution.config expected.
- persist_file parameter: when set, batch metadata and Globus Compute
  task UUIDs are written to JSON after registration and after results
  are cached, and loaded on init. Allows MCP servers to recover job
  state across restarts.
- TrackedTask.globus_task_id and TrackedTask.future are both optional;
  loaded-from-disk batches have no in-memory Future and are queried
  via the Globus Compute Client directly in get_status.
- Lazy Globus Compute Client with a separate gc_lock for thread safety.
- _wait_for_globus_task_ids polls each ComputeFuture briefly after
  submission to capture the Globus task_id assigned asynchronously
  by the Executor background thread.
- cancel_batch / cleanup_old_batches handle the no-future case.
- init_backend now accepts tracker_kwargs= and forwards it to
  JobTracker(...) in _ensure_backend. Callers can pass
  persist_file= so MCP servers recover job state across restarts.
- set_pre_submit_hook(hook): hook receives each TaskSpec before
  backend.submit() and returns a (possibly mutated) one. Lets a
  server centralise transport concerns -- inline-structure embedding
  for local-submit-to-remote-worker, remote-path rewriting -- instead
  of repeating that logic in every tool body. Wired into the
  @tool, @ensemble_tool, and @schema_fanout_tool submit paths.
- @schema_fanout_tool(worker=...): the decorated function is an
  expander (ensemble schema -> list of per-item args). The framework
  calls worker(item) on the backend for each item and gathers
  results. Preserves the ensemble schema as the agent-facing API
  (one tool call, server-side fanout), complementing @ensemble_tool
  which exposes list[Schema] for callers that want client-side
  enumeration.
…ersistence

- mace_input_schema_ensemble / graspa_input_schema_ensemble: new
  remote_structure_directory field for pre-staged HPC files (paired
  with the upcoming transfer_files tool). input_structure_directory
  now defaults to empty string so callers can pass either.
- mace_input_schema/_ensemble model description spells out that
  'mace_mp' is the calculator type, not a model name -- LLMs were
  confusing the two.
- Nullable schema fields (driver, model, wall_time) typed as
  str|None / float|None for correct OpenAPI schema generation.
- GlobusComputeBackend._ensure_executor re-creates the Executor when
  it has been shut down (e.g. after a remote task failure). Uses
  getattr() so we don't depend on the SDK's private _stopped attr
  existing.
- check_endpoint_status logs exc_info on failure for easier debugging.
- xanes_mcp_hpc: JobTracker(persist_file=~/.chemgraph/xanes_jobs.json)
  so XANES job state survives MCP server restarts. Instructions
  updated to tell the LLM to surface batch_ids to the user.
- execution/globus_transfer.py: GlobusTransferManager wraps the
  globus_sdk TransferClient with token caching, batched
  transfer_files / wait_for_transfer / check_transfer_status /
  list_remote_directory. Lazy globus_sdk import, lazy auth.
- execution/config.get_transfer_manager(): builds a manager from
  [execution.globus_transfer] in config.toml with env var overrides
  (GLOBUS_TRANSFER_SOURCE_ENDPOINT_ID, _DESTINATION_ENDPOINT_ID,
  _DESTINATION_BASE_PATH). Returns None when not configured so MCP
  servers can skip registration silently.
- mcp/transfer_tools.register_transfer_tools(): registers
  transfer_files, check_transfer_status, list_remote_files on a
  FastMCP/CGFastMCP server. Uses mcp.add_tool() (not the
  backend-submitting @tool() decorator) because these are
  orchestration tools, not compute tasks -- they call the Globus
  Transfer API directly from the MCP server process.
- get_backend() globus_compute endpoint_id fallback now treats
  empty-string endpoint_id as unset, matching the GLOBUS_COMPUTE_
  ENDPOINT_ID env-var override behaviour.
…GFastMCP

run_mace_single and run_mace_ensemble were collapsed to bare
run_mace_core(params) calls in PR #127, dropping inline-structure
embedding, remote-path support, JobTracker persistence, and the
Globus Transfer registration that 51ba171 had built. This restores
all of that on top of the new CGFastMCP framework.

- Worker is now a separate function _mace_worker(job: dict) that
  handles two transport keys on the worker FS: remote_structure_file
  (use the path directly) and inline_structure (materialise an
  AtomsData dict to a temp XYZ). Embeds full_output back into the
  result for inline calls so callers do not need remote FS access.
- Pre-submit hook _mace_transport_hook centralises the schema ->
  job-dict conversion, mace_mp -> medium-mpa-0 model normalisation,
  and inline embedding (when the input file exists on the
  submitting host). Hook rewrites task.callable from run_mace_single
  to _mace_worker so the LLM still sees a clean schema-shaped tool.
- run_mace_ensemble switches to @schema_fanout_tool with a
  server-side expander, preserving the directory-driven UX
  (single LLM call instead of N). Local mode enumerates files via
  resolve_structure_files; remote mode submits a backend probe to
  ls remote_structure_directory and builds remote_structure_file
  per item.
- extract_output_json registered via mcp.add_tool() (orchestration,
  no backend wrap). transfer_files/check_transfer_status/
  list_remote_files registered conditionally when
  get_transfer_manager() finds [execution.globus_transfer] config.
- __main__ now wires tracker_kwargs={persist_file: ~/.chemgraph/
  mace_jobs.json} so MACE batches survive MCP server restarts.
- Drop `from __future__ import annotations`: forward refs break
  FastMCP's signature introspection because the wrapper's __globals__
  is cg_fastmcp's, not the tool module's.
Wraps the Academy distributed agent framework with ChemGraph LLM
agents for federated HPC screening workflows. Decoupled from the
existing pipeline -- no chemgraph.cli / chemgraph.agent / chemgraph.eval
references; only the lazily imported chemgraph.agent.llm_agent.ChemGraph.

- ChemGraphAgent: Academy Agent wrapping a single ChemGraph instance,
  exposes run_query / get_info actions.
- ScreeningAgent: iterates a molecule batch, writes per-result JSONs
  for fault-tolerant aggregation. Failed-molecule records now store
  str(exc) so the actual exception message survives.
- CoordinatorAgent: polls a results dir, optionally analyses results
  via an LLM, suggests follow-up molecules.
- AcademyConfig + build_manager: bridge config.toml to Academy
  Manager / Exchange / Launcher (local, Redis, Parsl, Globus Compute).
- RateLimiter: stdlib async token-bucket for shared per-provider LLM
  quotas across agents.

Lazy imports in __init__.py let the package load without the optional
academy-py dependency; ChemGraphAgent / ScreeningAgent / CoordinatorAgent
raise ModuleNotFoundError on access if academy-py is missing, while
AcademyConfig and RateLimiter remain usable.

pyproject's academy optional-dep + pytest marker are already in HEAD
(commit 04bcc8a). tests/test_academy.py and scripts/academy_example/
remain untracked and will land in follow-ups.
- globus_transfer.py: disambiguate same-basename inputs with a numeric
  suffix so two files that share a name (e.g. /a/in.cif and /b/in.cif)
  don't silently overwrite each other on the remote collection.
- job_tracker.py: promote the "no Globus task_id within timeout"
  message to a warning at submit time, and emit a per-task warning at
  reload time for batches restored without a task_id (those tasks
  cannot be queried via the Globus Compute API and would otherwise be
  silently orphaned across server restarts).
- globus_compute_backend.py: catch "executor stopped" exceptions in
  submit(), rebuild the Executor, and retry once. The previous
  _ensure_executor relied on the SDK's private _stopped attribute,
  which fails silently if the SDK exposes the shutdown state
  differently.
- cg_fastmcp.py: wrap _apply_pre_submit_hook in try/except and re-raise
  hook failures as a ValueError naming the hook and task_id so they
  surface as a structured tool error instead of an opaque traceback.
Both servers now mirror the mace_mcp_hpc.py pattern:
- CGFastMCP with lazy backend initialisation via init_backend(); the
  worker subprocesses re-importing the module no longer instantiate a
  backend at import time.
- Job-management tools (check_job_status, get_job_results, list_jobs,
  cancel_job, check_endpoint_status) are auto-registered by
  CGFastMCP._register_job_tools; the external register_job_tools call
  is dropped.
- __main__ wires init_backend(tracker_kwargs={"persist_file": ...})
  and pairs run_mcp_server with shutdown_backend in finally. This also
  closes a real bug in graspa_mcp_hpc.py, which was instantiating
  JobTracker() with no persist_file and silently losing job state
  across restarts despite the server's instructions promising
  persistence.
- Globus Transfer tools (transfer_files, check_transfer_status,
  list_remote_files) are registered on both servers when the transfer
  manager is configured, matching the existing MACE behaviour.
- gRASPA expander now supports remote_structure_directory the same way
  MACE does: a one-shot probe task lists CIFs on the remote endpoint
  and the worker reads them directly from the staged path.
- Ensemble flows use the schema_fanout_tool decorator; per-job structure
  metadata is propagated through the worker output (since the framework
  meta is only the index).

Legacy *_mcp_parsl.py modules now raise a DeprecationWarning at import
pointing to the *_hpc.py replacement; they remain functional because
scripts/mcp_xanes_example/ still imports xanes_mcp_parsl.
…us Compute

Introduce a unified execution module with an abstract ExecutionBackend
interface and TaskSpec model, supporting four backends: local
(ProcessPoolExecutor), Parsl, EnsembleLauncher, and Globus Compute.

Includes config factory with resolution order (args > env > config.toml),
HPC configs loader, comprehensive tests, and pytest --run-globus-compute
option for live endpoint tests.
Remove dead num_nodes=1 after raise in aurora_parsl.py and fix
misleading error message. Set _initialized=False at start of
EnsembleLauncherBackend.shutdown() to prevent submitting to a
partially torn-down backend.
When backend=globus_compute, MCP tools now return immediately after
submitting jobs to the remote HPC endpoint instead of blocking until
completion. A new JobTracker tracks submitted futures across tool calls,
and new MCP tools (check_job_status, get_job_results, list_jobs,
cancel_job) let the LLM agent poll for progress and retrieve results.

Non-Globus backends (local, Parsl, EnsembleLauncher) are unchanged and
continue to block until results are ready.

Key changes:
- Add is_async_remote property to ExecutionBackend (True for Globus)
- Add check_endpoint_status() health check to GlobusComputeBackend
- Add JobTracker with batch registration, status, results, cleanup
- Add submit_or_gather() utility that branches on backend type
- Add optional timeout parameter to gather_futures()
- Add register_job_tools() to wire job tools into any MCP server
- Integrate tracker into MACE, XANES, and gRASPA MCP servers
- Add CGFastMCP: FastMCP subclass with integrated execution backend,
  lazy init, built-in job tools, @tool() and @ensemble_tool() decorators
- Refactor EnsembleLauncherBackend with client-only mode (shared
  orchestrator via checkpoint_dir) and managed mode
- Update get_backend() to route client_only vs managed EL initialization
- Rewrite mace_mcp_hpc.py to use CGFastMCP decorators
- Clean up parsl_tools.py: remove dead code, use stdlib logging
- Fix __main__ pickle issue via _fix_module_for_pickle + sys.modules alias
- Add client-only mode demo cell to notebook 3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mport

- parsl_tools.run_mace_core: stop swallowing exceptions and returning None.
  run_ase_core already returns a structured failure dict on simulation
  errors, and programmer errors should propagate.
- cg_fastmcp.ensemble_tool: raise TypeError with a clear message when the
  decorated function does not have exactly one parameter, instead of
  crashing with IndexError at decoration time.
- ensemble_launcher_backend: soft-import ensemble_launcher and defer the
  failure to construction / call time. SYSTEM_CONFIG_REGISTRY is now a
  lazy view backed by builder functions so the module loads cleanly
  without EL installed, restoring the deferred-error behaviour callers
  of chemgraph.execution.config expected.
- persist_file parameter: when set, batch metadata and Globus Compute
  task UUIDs are written to JSON after registration and after results
  are cached, and loaded on init. Allows MCP servers to recover job
  state across restarts.
- TrackedTask.globus_task_id and TrackedTask.future are both optional;
  loaded-from-disk batches have no in-memory Future and are queried
  via the Globus Compute Client directly in get_status.
- Lazy Globus Compute Client with a separate gc_lock for thread safety.
- _wait_for_globus_task_ids polls each ComputeFuture briefly after
  submission to capture the Globus task_id assigned asynchronously
  by the Executor background thread.
- cancel_batch / cleanup_old_batches handle the no-future case.
- init_backend now accepts tracker_kwargs= and forwards it to
  JobTracker(...) in _ensure_backend. Callers can pass
  persist_file= so MCP servers recover job state across restarts.
- set_pre_submit_hook(hook): hook receives each TaskSpec before
  backend.submit() and returns a (possibly mutated) one. Lets a
  server centralise transport concerns -- inline-structure embedding
  for local-submit-to-remote-worker, remote-path rewriting -- instead
  of repeating that logic in every tool body. Wired into the
  @tool, @ensemble_tool, and @schema_fanout_tool submit paths.
- @schema_fanout_tool(worker=...): the decorated function is an
  expander (ensemble schema -> list of per-item args). The framework
  calls worker(item) on the backend for each item and gathers
  results. Preserves the ensemble schema as the agent-facing API
  (one tool call, server-side fanout), complementing @ensemble_tool
  which exposes list[Schema] for callers that want client-side
  enumeration.
…ersistence

- mace_input_schema_ensemble / graspa_input_schema_ensemble: new
  remote_structure_directory field for pre-staged HPC files (paired
  with the upcoming transfer_files tool). input_structure_directory
  now defaults to empty string so callers can pass either.
- mace_input_schema/_ensemble model description spells out that
  'mace_mp' is the calculator type, not a model name -- LLMs were
  confusing the two.
- Nullable schema fields (driver, model, wall_time) typed as
  str|None / float|None for correct OpenAPI schema generation.
- GlobusComputeBackend._ensure_executor re-creates the Executor when
  it has been shut down (e.g. after a remote task failure). Uses
  getattr() so we don't depend on the SDK's private _stopped attr
  existing.
- check_endpoint_status logs exc_info on failure for easier debugging.
- xanes_mcp_hpc: JobTracker(persist_file=~/.chemgraph/xanes_jobs.json)
  so XANES job state survives MCP server restarts. Instructions
  updated to tell the LLM to surface batch_ids to the user.
- execution/globus_transfer.py: GlobusTransferManager wraps the
  globus_sdk TransferClient with token caching, batched
  transfer_files / wait_for_transfer / check_transfer_status /
  list_remote_directory. Lazy globus_sdk import, lazy auth.
- execution/config.get_transfer_manager(): builds a manager from
  [execution.globus_transfer] in config.toml with env var overrides
  (GLOBUS_TRANSFER_SOURCE_ENDPOINT_ID, _DESTINATION_ENDPOINT_ID,
  _DESTINATION_BASE_PATH). Returns None when not configured so MCP
  servers can skip registration silently.
- mcp/transfer_tools.register_transfer_tools(): registers
  transfer_files, check_transfer_status, list_remote_files on a
  FastMCP/CGFastMCP server. Uses mcp.add_tool() (not the
  backend-submitting @tool() decorator) because these are
  orchestration tools, not compute tasks -- they call the Globus
  Transfer API directly from the MCP server process.
- get_backend() globus_compute endpoint_id fallback now treats
  empty-string endpoint_id as unset, matching the GLOBUS_COMPUTE_
  ENDPOINT_ID env-var override behaviour.
…GFastMCP

run_mace_single and run_mace_ensemble were collapsed to bare
run_mace_core(params) calls in PR #127, dropping inline-structure
embedding, remote-path support, JobTracker persistence, and the
Globus Transfer registration that 51ba171 had built. This restores
all of that on top of the new CGFastMCP framework.

- Worker is now a separate function _mace_worker(job: dict) that
  handles two transport keys on the worker FS: remote_structure_file
  (use the path directly) and inline_structure (materialise an
  AtomsData dict to a temp XYZ). Embeds full_output back into the
  result for inline calls so callers do not need remote FS access.
- Pre-submit hook _mace_transport_hook centralises the schema ->
  job-dict conversion, mace_mp -> medium-mpa-0 model normalisation,
  and inline embedding (when the input file exists on the
  submitting host). Hook rewrites task.callable from run_mace_single
  to _mace_worker so the LLM still sees a clean schema-shaped tool.
- run_mace_ensemble switches to @schema_fanout_tool with a
  server-side expander, preserving the directory-driven UX
  (single LLM call instead of N). Local mode enumerates files via
  resolve_structure_files; remote mode submits a backend probe to
  ls remote_structure_directory and builds remote_structure_file
  per item.
- extract_output_json registered via mcp.add_tool() (orchestration,
  no backend wrap). transfer_files/check_transfer_status/
  list_remote_files registered conditionally when
  get_transfer_manager() finds [execution.globus_transfer] config.
- __main__ now wires tracker_kwargs={persist_file: ~/.chemgraph/
  mace_jobs.json} so MACE batches survive MCP server restarts.
- Drop `from __future__ import annotations`: forward refs break
  FastMCP's signature introspection because the wrapper's __globals__
  is cg_fastmcp's, not the tool module's.
tdpham2 and others added 15 commits June 17, 2026 23:34
# Conflicts:
#	src/chemgraph/hpc_configs/aurora_parsl.py
#	src/chemgraph/tools/ase_core.py
demo_parsl_in_job_agent.py forced model_name="argo:gpt-5.4" and a
local argoapi base_url, mirroring the temp config that had also landed
in demo_ensemble_launcher_in_job_agent.py. Restore model selection from
the --model flag (the amain(model=...) parameter) and drop the hardcoded
base_url so the demo honours the user's chosen model.
TestELSystemConfigCrux asserted the exact Crux SystemConfig shape
(ncpus==128, CPU-only) and the registry membership of "crux". These
are tied to one specific machine's hardware layout and don't belong in
the portable unit-test suite. The polaris references elsewhere are left
as-is since they only pass "polaris" as an arbitrary system string to
exercise generic GlobusCompute behaviour.
EnsembleLauncher is an optional, HPC-only dependency (not on PyPI for
Python 3.12), so instantiating EnsembleLauncherBackend() raised
ImportError and hard-failed all nine TestELBackend cases in any env
without it. Add pytest.importorskip("ensemble_launcher") in setup_class
so the class skips cleanly, matching the guard already used by the
GlobusCompute tests.
Update existing backends (Parsl, EL, Globus Compute, Globus Transfer) to ChemGraph
The chemgraph.academy package eagerly imported ChemGraphLogicalAgent,
which in turn imports academy.agent. Any consumer that touched the
package -- including chemgraph.cli.trace's --trace-dir code path and
pytest collection -- crashed when the optional [academy] extra was
not installed, despite the rest of the package (campaign spec,
prompt profile, event log) being pure stdlib + pydantic.

Fix in two layers:

* src/chemgraph/academy/__init__.py and
  src/chemgraph/academy/core/__init__.py both grow a __getattr__-
  based lazy resolver for ChemGraphLogicalAgent. The pure modules
  stay eager so their public surface (~11 symbols) is reachable on
  a CPU-only checkout. Accessing the lazy symbol without the extra
  installed raises ImportError with an actionable
  `pip install 'chemgraph[academy]'` hint.

* tests/test_academy_*.py and tests/test_tool_adapter_validation.py
  (9 modules total) gain `pytest.importorskip("academy")` at the
  top, so they skip cleanly when the extra is absent instead of
  erroring at collection time.

Validated:
  * Without academy-py: chemgraph.academy imports, all eager symbols
    resolve, cli.trace loads, lazy ChemGraphLogicalAgent access
    raises with hint, all 9 academy test modules skip cleanly.
  * With academy-py: 48/48 academy tests pass.

Two-tier __init__ was necessary because pulling
chemgraph.academy.core.campaign transitively runs core/__init__.py,
which previously eager-imported core.agent. Both __init__s now
follow the same lazy pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f1593ab ("revert: restore llm_agent.py to pre-academy shape") was
meant to drop temporary event-callback wiring and turn-primitive
imports, but its rewrite also dropped the calculator-availability
injection that was on the file at the dev-globus fork point. The
deletion was a merge-conflict casualty, not an intentional removal:
get_calculator_selection_context() in schemas/ase_input.py:145 was
left as dead code, and the two regression tests
test_single_agent_initialization_injects_calculator_availability
(in tests/test_graph_constructors.py and tests/test_graphs.py) have
been failing on dev-globus ever since.

Restore the 28-line wiring block verbatim from
origin/dev:src/chemgraph/agent/llm_agent.py:464-490, plus the
matching `from chemgraph.schemas.ase_input import
get_available_calculator_names, get_calculator_selection_context,
get_default_calculator_name` import at the top.

Position in __init__: after the human-supervised system_prompt
adjustment (line 324) and before the structured-output gate (line
326). All three instance attributes the block references
(self.workflow_type, self.planner_prompt, self.executor_prompt) are
set earlier in __init__ (lines 299, 308, 309), so the wiring order
is safe.

Both calculator-injection regression tests now pass; the 48-test
academy suite is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(academy,agent): PR-120 follow-up — optional-dep contract + calculator wiring
Prepare PR #120 for merge by dropping a non-functional workflow and
fixing event/HPC-pickle regressions. The academy optional-dependency
and calculator-context fixes originally bundled here are already on
dev-globus (b5cac8b, d293cea), so this commit no longer carries them.

- Remove single_agent_architector workflow: it was not registered in
  workflow_map and required a tools module that does not exist. Drop the
  graph, the CLI entry, and the related test parameters (including a
  stale graspa_agent case).
- events: only emit llm_decision when the model requested tool calls;
  a plain text answer has no decision to report.
- mcp: add explicit pickle-by-reference fix for the gRASPA
  _ls_remote_files TaskSpec callable; document that decorator-registered
  worker callables are already fixed by schema_fanout_tool.
- tests: update the MACE worker test for the dropped full_output field;
  note in conftest that academy-only modules self-skip via importorskip.
The ground-truth generator's extract_output_json branch read a nested
final_result['result'] key for the thermo/vib/ir drivers, but
extract_output_json returns the flat ASEOutputSchema where those fields
live at the top level. As a result vibrational frequencies and Gibbs free
energy were silently dropped (e.g. q29 stored a single-point energy in
place of frequencies; q17/q20/q30 stored single-point energy labelled as
Gibbs free energy). Read these fields from the top level instead, and also
accept the schema's dipole_value key.

The structured-output judge's _compare_scalar ignored the property field,
so a single-point energy within 5% of the Gibbs free energy passed
silently. Add a normalized property-category check (gibbs/enthalpy/entropy/
dipole/energy) that tolerates phrasing differences but catches mismatched
quantities. Also flag all-null expected ground truth as a failure with a
parse_error instead of a trivial pass.

Remove stale mcp_example scripts under new_eval.
The default dataset shipped under chemgraph/eval/data/ was stale and
produced by the buggy ground-truth generator. Remove it; callers should
pass an explicitly regenerated dataset via --dataset.
Extend the demo layer so run_ase (general ASE, selectable calculator) and
run_graspa (GCMC) run across all execution backends, alongside the existing
MACE thermo screen. No changes to the execution abstraction -- it was already
backend-agnostic.

- New backend-agnostic ASE MCP server (mcp/ase_mcp_hpc.py) mirroring
  mace_mcp_hpc.py: run_ase_single / run_ase_ensemble with a selectable
  calculator, inline/remote transport hook, and pickle-by-reference guards
  for Globus Compute.
- New ase_input_schema_ensemble; calculator coercion factored into a shared
  helper used by both the single and ensemble schemas.
- Demo harness (scripts/demo/_demo_chemistry.py) generalized into a workload
  registry (thermo / ase / graspa): build_ase_job, build_graspa_job,
  workload-aware submit_and_collect, inline full-output wrapper, gRASPA
  property extractor and summary table, ASE/gRASPA prompts, shared CLI helpers.
- All demo_*_direct.py / demo_*_agent.py gain --workload (+ --calculator,
  --graspa-cifs) and per-workload MCP server selection. gRASPA is gated to HPC
  backends (SYCL binary path is baked into graspa_core); it exits non-zero on
  the local backend.
Add pluggable execution backends, HPC MCP servers, and Academy multi-agent campaigns
config.toml was added on main (e9e83bc "Restore root config.toml") but
never existed on dev, which still gitignored it. Restore the file from
main's tracked copy and drop the config.toml entry from .gitignore so
both branches agree and the default config stays version-controlled.
@tdpham2 tdpham2 self-assigned this Jul 1, 2026
tdpham2 and others added 4 commits July 1, 2026 15:33
Test fixes:
- llm_agent: select the RAG/XANES default prompt via a prompt_is_default
  flag captured before the unsupervised-mode prompt mutation, so the
  default prompts are no longer masked by the rewritten system_prompt.
- test_job_tracker: use asyncio.run instead of
  asyncio.get_event_loop().run_until_complete, which raises on Python 3.12.

Lint fixes (ruff check .):
- Move deprecation warnings.warn below imports in the deprecated parsl
  MCP modules (E402).
- Hoist chemgraph.agent.events import to the top of turn.py (E402).
- Add typing.Any import in academy daemon (F821).
- Drop unused locals/imports and collapse multi-import lines
  (F841/F401/F541/E401).
llm_agent.py kept an old naive serialize_state that recursed through
__dict__ with no depth or cycle guard, while turn.py has a hardened
version with cycle detection and max_depth. On a cyclic LangGraph state
this overflowed the (smaller) Windows stack in write_state.

Remove the duplicate and import the hardened serialize_state from
turn.py so write_state, the state return path, and cli/commands.py all
share one implementation.
@tdpham2
tdpham2 requested a review from keceli July 2, 2026 14:36
tdpham2 and others added 7 commits July 7, 2026 15:09
Writers (smiles_to_coordinate_file, run_ase output) route relative paths
through _resolve_path into CHEMGRAPH_LOG_DIR, but readers opened them from
cwd -> FileNotFoundError on bare names like "water.xyz".

Add _resolve_existing_path() and use it in run_ase, extract_output_json,
file_to_atomsdata, generate_html. Prefers an existing path, else the
log-dir location, else raw (so missing-file errors still surface).

Add regression tests (EMT, no network).
Extends the CHEMGRAPH_LOG_DIR path resolution from the LangChain tool
readers to the MCP servers and the graspa/xanes cores, so a small model
that echoes back a bare filename ("water.xyz") for a file a sibling tool
wrote into the log dir still resolves it instead of failing with
FileNotFoundError.

- execution/utils.resolve_structure_files: resolve each listed filename
  against the log dir (shared by the ase/mace/graspa/xanes _hpc ensemble
  tools). Directory inputs are left unchanged.
- mcp/ase_mcp_hpc.py, mcp/mace_mcp_hpc.py: _embed_inline_if_local resolves
  the bare name on the submitting host before its local-vs-remote check and
  stores the resolved absolute path back into the job. Worker code unchanged.
- tools/graspa_core.py, tools/xanes_core.py: resolve input_structure_file via
  _resolve_existing_path instead of Path(...).resolve(), so the single-
  structure _hpc path (which delegates here) also resolves bare names. The
  ase/mace cores already did this via run_ase_core.
- mcp/data_analysis_mcp.py: aggregate_simulation_results resolves each input.
- mcp/hpc_misc_mcp.py: inspect_json checks the log dir before its nearby-files
  fallback.
- mcp/graspa_mcp_parsl.py, mcp/xanes_mcp_parsl.py: same fallback in their
  copied list-resolution logic (deprecated servers; included for consistency).

The general server (mcp/mcp_tools.py) already delegates to the fixed core and
returns absolute paths; added tests to pin that behavior.

The _hpc worker functions are deliberately left untouched: when the backend
shares the filesystem the worker delegates to the (now-fixed) cores, and when
it does not the bare name is resolved on the submitting host before submission.
The multi-agent human-in-the-loop default changed to False, so
unified_planner_router and construct_multi_agent_graph no longer route to
/ include the human_review path unless human_supervised=True is passed.
Update the three multi-agent human-interrupt tests to opt in explicitly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correctness/robustness fixes surfaced during review of the dev integration:

- agent/turn.py: _tool_message_name no longer counts a named non-tool
  message (e.g. a named AIMessage/HumanMessage in the Academy multi-agent
  flow) as an executed tool, which had polluted executed_tool_names.
- execution/job_tracker.py:
  * _save() tolerates non-JSON-serializable task results (default=str +
    guard) so get_status/get_results can't crash when persist_file is set
    (used by all *_mcp_hpc.py servers).
  * register_batch() no longer blocks the full 3s globus wait for plain
    concurrent.futures.Future objects (only ComputeFutures carry task_id).
  * get_status() mutates task state under the (now re-entrant) lock,
    matching the documented thread-safety contract.
- mcp/server_utils.py: run streamable-HTTP MCP servers with uvicorn
  ws="none"; the HTTP transport doesn't use WebSockets, and loading
  uvicorn's ws protocol failed against the websockets<11 pinned by
  pyppeteer (missing ServerProtocol) -- broke every *_mcp_hpc.py server.

Testing:
- New regression tests: test_turn_executed_tools.py, test_job_tracker_hardening.py.
- Fixed stale test_tool_adapter_validation: drain the queued background
  delivery before asserting peer receipt.
- New CI job 'test-extras' installs .[academy,parsl,globus_compute] and runs
  the Academy + execution-backend tests (mocks/local subprocesses; live
  globus/LLM stay gated). Core: 270 passed; with extras: 318 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reflects the dev->main integration (execution backends, Academy multi-agent,
CGFastMCP, HPC/Globus) landing via #139.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix relative path resolution in tool file readers
@keceli
keceli merged commit f7581c3 into main Jul 19, 2026
30 checks passed
@tdpham2 tdpham2 mentioned this pull request Jul 20, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants