Conversation
`molexp.Path` subclasses `pathlib.PurePosixPath` so paths under a remote (RemoteFileSystem) workspace can use the same `/` arithmetic without exposing local-only I/O methods that would silently hit the wrong filesystem. Exported from `molexp` and listed in CLAUDE.md's allowed cross-layer primitives. Foundation for the follow-up PRs that migrate Folder.path()/.resolve() return types and unify the agent layer's "local-only" divergence. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce a `PathArg = str | os.PathLike[str]` type alias and update every
method signature in the `FileSystem` Protocol + `LocalFileSystem` +
`RemoteFileSystem` to accept it. Implementations normalize via
`os.fspath()` at the boundary so the rest of the code can pass either
`molexp.Path` or `str`.
`LocalFileSystem` already wrapped each path with `pathlib.Path()` so it
needed minimal changes; `RemoteFileSystem` does string-level POSIX
manipulation, so each entry point now `os.fspath()`s its input before
the `.strip("/")` / `.rsplit("/")` calls.
Backward-compatible: callers passing plain `str` continue to work
unchanged. This is the bridge that lets PR 3 flip Folder.path() /
.resolve() to return `molexp.Path` without touching every caller.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Migrate every workspace-layer Folder subclass to return `molexp.Path` (a `PurePosixPath` subclass) instead of `str`: - `Folder.path()` / `.resolve()` / `.child_dir()` / `_container_dir()` - `Workspace.root` / `Workspace.resolve()` - `Project.project_dir`, `Experiment.experiment_dir`, `Run.run_dir` - `CacheFolder.entry_path()` The previous `str` return type was a hedge against accidental `folder.path().exists()` calls hitting the local filesystem on a remote workspace. `molexp.Path` removes that hazard structurally — it has no I/O methods — so we recover the `/` operator and path arithmetic without re-introducing the silent-local-FS bug. Drop the long "str vs Path divergence" doc block from `folder.py`: agent-layer subclasses no longer need to diverge (see PR 4). Coerce to `pathlib.Path` at every site that does genuine local I/O (`pathlib.Path` for `.exists()` / `.mkdir()` / `.read_text()`): - workspace assets (`AssetManifest`, `DataAssetLibrary`) - server routes (`_scope.py`, `run.py`, `responses.py`) - `tree_monitor._read_run_json` - `RunContext` (its `work_dir` is local-only by design) - `_make_execution_id` in workflow runtime Update the workspace / server / cli / workflow tests to add `from pathlib import Path` imports and wrap workspace paths with `Path(...)` at the I/O boundary. No assertion semantics change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Agent-layer Folder subclasses now use `self._fs` for every I/O call instead of binding to `pathlib.Path`. They no longer assume "local filesystem" and can mount on a `RemoteFileSystem`-backed workspace. - `Agent.child_dir()` / `AgentSession.child_dir()` / `PlanFolder.child_dir()`: use `parent._fs.join(...)`, return `molexp.Path` - `from_disk(child_dir: PathArg, ...)`: pass `fs=parent._fs` into `_load_metadata` so reads go through the workspace's transport - `materialize` / `save`: `self._fs.mkdir` + `_save_metadata(fs=self._fs)` - `AgentSession.messages_path`: built via `self._fs.join` - `read_messages` / `write_messages`: use `self._fs.exists / read_bytes / write_bytes / remove / mkdir` — no more `path.exists()` / `tmp.replace()` - Drop the "local-only override" / "agent state is local-only by design" docstrings — that divergence is gone PlanFolder is fundamentally local (it manages generated source + on-disk YAML artefacts), so its `_ensure` / `_resolve` helpers and public `*_dir()` accessors continue to return `pathlib.Path`. The Folder hooks (`resolve` / `child_dir` / `path`) return `molexp.Path` to satisfy the framework contract. New acceptance test (`tests/test_agent/test_remote_session.py`) uses a recording-spy `_SpyFileSystem` wrapping `LocalFileSystem` to assert that mount, message round-trip, empty read, write-then-remove, and from_disk reload all route through fs methods (no direct pathlib calls). 5 tests, all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`preflight.py` was reaching `from pydantic_ai.mcp import MCPServerStdio` inside a function — function-local but the AST-based import guard (`tests/test_agent/test_import_guard.py::test_pydantic_ai_imports_confined_to_pydanticai_subtree`) doesn't care about scoping; any import statement outside `_pydanticai/` trips it. Extract the handshake helper into `_pydanticai/mcp.check_stdio_handshake` (natural home — sits next to `build_mcp_server`); `preflight._check_mcp_stdio_handshake` becomes a one-line delegating shim. Behavior unchanged. Restores the import-boundary firewall: no `pydantic_ai` imports outside `agent/_pydanticai/`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A previous CLI reorganisation moved init under `workspace <TARGET>` and left `runs prune` / `target` subcommands and the top-level `init` shortcut unregistered. Tests for them have been red for a while. - `molexp init [PATH]`: new thin shim in `cli/init_cmd.py` delegating to `Workspace(path).materialize()`. Idempotent. Defaults PATH to cwd. - `molexp runs prune ...`: `cli/prune.py` had a `register(run_app)` function but nothing called it — wire it under a new `runs` subcommand group. Also coerce `run.run_dir` to pathlib.Path inside prune.py for the local-FS `.exists()` / `.relative_to()` calls (molexp.Path is a PurePosixPath subclass with no I/O methods). - `molexp target add/list/remove/test`: new `cli/target_cmd.py` built on top of the existing `workspace/targets.py` CRUD helpers. Mirrors the argv the tests expect: `--scratch`, `--scheduler`, `--host`, `--port`, `--identity-file`, `--path`. `test` runs a 3-step smoke probe (command exec → mkdir → file round-trip) over the target's transport. Also delete `tests/test_cli/test_run_cmd_helpers.py` — it imports `molexp.cli.run_cmd` which was deleted in the same reorganisation; the test only triggered a pytest collection error. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The unified-FileSystem refactor (365008f) intentionally stopped passing RunStorePersistence into pydantic-graph's runner (the new GraphBuilder API doesn't expose `iter_from_persistence`), but it also accidentally dropped the construction itself. Result: `<run_dir>/executions/<id>/` never appeared, even though the runtime's module docstring still claims "per-frame snapshots are still written by RunStorePersistence for observability". Restore the construction (without re-injecting it as the graph runner's persistence) in both `GraphWorkflowRuntime.execute` and `.start`. The class's `__init__` writes the initial workflow.json — enough to satisfy the observability contract: tooling can rely on the per-execution directory always existing after a run, even if per-frame updates aren't flowing through. Fixes `tests/test_workflow/test_runtime.py::*::test_runtime_runs_with_ duck_typed_run_context_no_workspace` and `::test_run_dir_kwarg_writes_ workflow_json`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three leftover files were sitting in the working tree: - `tmp/`: pytest's `tmp_path` output left by a stray `pytest --basetemp=tmp/` run. Pure junk. Deleted and added to `.gitignore` so it can't return. - `tests/test_workflow/test_graph_start_node.py`: regression test for the pydantic-graph 1.x `inputs=` requirement (the `_GRAPH.run` helpers `_run_graph` / `_iter_graph` exist solely to keep this parameter from getting forgotten at the three call sites). Real guard against a real recurring bug — committed. - `tests/test_workspace/test_from_disk_invariants.py`: regression test for Folder `from_disk` overrides losing `_fs` on the reconstructed attrs dict (manifests as `AttributeError: 'Project' object has no attribute '_fs'` only after a workspace reload). Validates the `base_from_disk_attrs` contract. Real guard — committed. Both regression tests pass cleanly (5/5). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ndpoints
Server-process descriptor registry for remote workspace roots — the
first of three sub-specs (remote-workspace-via-ui-01-server-registry)
that let the UI mount a workspace whose root lives on a remote HPC
node. Active-workspace switching (sub-spec 02) and the Settings UI
(sub-spec 03) build on this surface.
* WorkspaceTarget — frozen pydantic descriptor (name / host / port /
identity_file / ssh_opts / root_path).
* WorkspaceTargetRegistry — server-process-scope CRUD backed by
~/.molexp/workspace_targets.json via atomic_write_json (crash-safe);
add raises ValueError on duplicate, get/remove raise KeyError.
* New routes on /api/workspace/targets — GET / POST / DELETE /
POST {name}/test. Connectivity probe mirrors /api/targets/{name}/test
(mkdir + file round-trip) and returns HTTP 200 + ok=false on failure
so the UI can render inline. Endpoints work before any workspace is
open (don't depend on get_workspace).
* schemas/workspace_targets.py — request/response models; re-imports
TargetTestCheck / TargetTestResponse from schemas/targets.py.
* dependencies.py — get_workspace_target_registry (lazy singleton) +
get_remote_fs_factory (test seam).
* Soak test (tests/test_workspace/test_workspace_remote_fs_soak.py)
exercises Workspace -> Project -> Experiment -> Run over a spy-wrapped
LocalFileSystem to verify every I/O routes through self._fs and the
non-LocalFileSystem branch skips .resolve() — retires the
medium-confidence librarian risk that Workspace(fs=RemoteFileSystem)
end-to-end CRUD was unverified.
49 new tests passing. ty + ruff clean on touched files. OpenAPI JSON
and ui/src/api/generated/ regen left out of this commit so sub-spec 02's
WorkspaceOpenRequest discriminator can land in a single coherent regen.
…witching
Second of three sub-specs (remote-workspace-via-ui-02-active-switching).
Makes the active workspace switchable between a local path and a
registered remote WorkspaceTarget descriptor while preserving the
existing {path, create_if_missing} request shape.
* WorkspaceOpenRequest -> discriminated union via pydantic v2 `Tag` +
callable `Discriminator` (back-compat: missing `kind` defaults to
"local"). New members: WorkspaceOpenLocalRequest /
WorkspaceOpenRemoteRequest exported alongside the alias.
* _workspace_cache rekeyed to `dict[tuple[str, str], Workspace]` —
local-vs-remote workspaces no longer collide.
* set_active_workspace_descriptor(name) + _workspace_descriptor_override
mirror set_workspace_path_override; mutual exclusion enforced on each
setter. _active_workspace_key centralises override precedence;
get_workspace branches on kind and builds Workspace(root_path, fs=...)
for the remote branch via target_to_filesystem_for_workspace_target.
* register_workspace_subscriber + _drain_workspace_subscribers — the
SSE-on-switch teardown seam. Both override setters drain
subscribers *before* reset_workspace_cache so closers can still
resolve the outgoing workspace. Sync and async closers supported;
closer failures are swallowed (best-effort drain).
* POST /api/workspace/open rewritten to dispatch on kind: local branch
preserves existing semantics; remote branch resolves through the
WorkspaceTarget registry (404 if absent) and never auto-creates the
remote root.
* Coverage: 11 unit tests pin discriminator + cache + mutual exclusion +
drain semantics; 1 integration test exercises local -> remote -> local
round-trip through TestClient and asserts state isolation.
ty + ruff clean on touched files. 142 -> 154 server tests passing.
TS-client regen (workspace open + targets models) lands in a follow-up
commit alongside the sub-spec 03 UI work.
Third of three sub-specs (remote-workspace-via-ui-03-ui-settings-page). Lands the user-facing UI for managing remote-workspace descriptors and switching the active workspace. Code-level scope only; the spec's four `ui_runtime` acceptance criteria (driven via `/mol:web` against `dev:mock`) remain pending and gate the spec to `code-complete`. * SettingsPage reorganized into a two-tab layout via the existing `@/components/ui/tabs` primitive — Remote workspaces (default) and Compute targets (existing ComputeTargetsPanel unchanged). * RemoteWorkspacesPanel mirrors ComputeTargetsPanel: list with Test / Set active / Delete actions; an inline ok/fail Test result block; Active badge on the currently-mounted descriptor; delete disabled on the active row with an explanatory title. * AddRemoteWorkspaceForm + AddRemoteWorkspaceDialog mirror the AddTargetForm/AddTargetDialog pattern (controlled-state inputs, `<Label htmlFor>` on every field, submit calls the regenerated WorkspaceService.createWorkspaceTarget…). * `app/state/workspaceSwitchEvents.ts` (new) — small `workspace-switching` CustomEvent emitter mirroring `mcpEvents.ts`. Fired on Set-active so long-lived workspace-bound resources (EventSource, file watchers) can tear down before the cache is invalidated. * MSW handlers — `mocks/handlers/workspace_targets.ts` (new) seeds three remote-workspace fixtures and answers GET/POST/DELETE/POST-test; `mocks/handlers/workspace.ts` extended to dispatch POST /api/workspace/open on `kind` (defaults to "local" for back-compat); a `mockWorkspaceTargetsMap` re-export lets the local workspace handler look up the remote descriptor name. * No hand-edits to `ui/src/api/generated/` — the regenerated workspace service / models from sub-spec 02 land in a separate regen commit. Status: code-complete. 4 `ui_runtime` criteria pending (Settings tab default, Add Remote happy path, Test pass/fail inline, Set-active flips badge + refreshes tree) — drive via `/mol:web` against the dev:mock server when ready.
…1/05) New agent-layer package molexp.agent.modes._planning — pure frozen-pydantic data models, the substrate the four-mode restructure (Plan/Author/Run/Review) builds on. Five clusters: IntentSpec, the typed PlanGraph DAG, CapabilityGraph, the explicit PlanState lifecycle + legal-transition table, and PlanDiff + ApprovalGate. Purely additive — no mode behaviour changes; 148 tests.
New src/molexp/agent/harness/ — the pi-inspired shared runtime every mode sits on: typed AgentEvent stream, Session append-only entry-tree + SessionStorage repository (jsonl + in-memory), context compaction via Router, ExecutionEnv subprocess abstraction, AgentHarness + uniform hook registry unifying the three ApprovalGates. AgentMode.run is now harness-based (async AgentEvent stream); AgentRunResult gains .events; AgentRunner builds/injects the harness and adds run_events(). The legacy codegen PlanMode and the ReviewMode placeholder are deleted — specs 03 & 06 rebuild them on the harness. ChatMode migrated. 1315 tests.
PlanMode rebuilt as a read-only typed planner running on the AgentHarness: a 7-stage plain-async pipeline (SynthesizeIntent -> ClarifyIntent -> ExploreCapabilities -> SynthesizeCandidates -> SelectPlan -> PreflightPlanGraph -> EmitApprovedPlan) emitting AgentEvents, with a plan-diff repair loop and the approve_direction gate via harness.approve. Produces a typed PlanGraph + ApprovedPlanHandoff; writes no executable code. PydanticAICapabilityProbe recreated (molmcp-backed) and wired so ExploreCapabilities probes capability in production. 1373 tests.
…4/06) AuthorMode materializes an approved typed PlanGraph into a validated experiment workspace: lowers PlanGraph -> molexp.workflow.WorkflowContract, generates workflow.py + per-task source + tests + IR + manifest, runs each generated task's test through the harness ExecutionEnv (isolated subprocess + hard timeout + confined cwd) with an LLM debug->repair loop, gates all file writes behind approve_materialization, and emits a MaterializedWorkspaceHandoff. Codegen + the deterministic WorkflowContract normalizer recreated against the typed surface. 1424 tests.
RunMode executes a materialized experiment workspace: demands the approve_execution gate, loads the workflow via its entrypoint, binds it to a workspace Run/RunContext and runs it through the public molexp.workflow API, projects per-step progress onto the typed PlanSteps (RunProgress), honours RetryPolicy on transient failures, and emits a PlanDiff / RepairEscalation on unrecoverable failure. PlanState gains running + completed with their legal transitions. 1492 tests.
…6/06) ReviewMode — a read-only harness-based AgentMode that reviews an existing plan / materialized workspace / completed run against the IntentSpec + typed PlanGraph. Three pure checkers (intent conformance, capability evidence, lifecycle consistency) feed a typed ReviewVerdict with per-step findings, an overall pass/fail/needs-changes outcome, and a PlanDiff that feeds the shared repair loop. Writes only its own verdict artefact. Completes the six-spec agent-modes-restructure chain. 1526 tests.
… agent layer
CLAUDE.md agent-layer charter, the Architecture agent section, and the
data-type-ownership table rewritten to describe the harness + four
pipeline modes (Plan/Author/Run/Review on AgentHarness, AgentMode.run as
an AgentEvent async generator). examples/agent/{chat,plan}_mode.py
rewritten against the new AgentRunner/harness API; both run clean.
Apply ruff format and fix pre-existing ruff check errors (ANN202 / ARG002 / ANN002 / ANN003 / RUF059) in files unrelated to the testability-driven-decomposition feature, so the lint gate is green.
PlanMode now recursively splits candidate plans until every step is small enough to carry an independent, low-cost isolated test. - _planning: PlanStep gains a required IsolatedTestSketch test_sketch - tasks_planning: refine_until_testable recursive split stage + the testability invariant in _CANDIDATE_SYSTEM_PROMPT - plan_graph_preflight: 8th structural check every_step_isolated_testable - author/codegen: tightened isolated-test prompt + test_sketch threading Bundled agent-layer fixes from the same work session: - review.py simplified to ReviewDecision / ReviewPolicy / cli_ask - AgentRunner gains approval= wiring the harness before_approval hook - capability probe: per-need bounded discovery, fixing UsageLimitExceeded - PlanMode intent/candidate prompts: identifier-style required_outputs
Add AgentMode.get_flowchart() -> str, a base-class method that renders a mode's stage pipeline as a Mermaid flowchart TD. - mode.py: frozen PipelineEdge / ModePipeline declarative types + the self-contained _render_pipeline_flowchart builder (no workflow/pg import) - AgentMode gains a pipeline class attribute + the inherited get_flowchart() - all five modes declare their pipeline next to name - fix: PlanMode's EmitApprovedPlan stage was never wrapped in harness.stage(); _run_approval is now bracketed so PlanMode emits 7 timed stages, matching its seven-stage docstring - per-mode no-drift test: AST-extracted harness.stage() literals must equal the declared pipeline.stages
Add InteractiveMode — the agent layer's first emergent mode: the LLM drives a read-only tool loop (read_file / list_directory / search_code, workspace-confined) via pydantic-ai's native Agent.iter(), exposed through the new Router.stream_agentic surface and three loop events (TokenDeltaEvent / ToolCallStartedEvent / ToolCallCompletedEvent). A /plan prefix or the run_plan_pipeline tool delegates to the structured PlanMode pipeline for auditable plan output — composition, not inheritance. Add the 'molexp agent' CLI command: a multi-turn REPL consuming run_events() with a rich per-event renderer in the CLI layer. Also fixes a pre-existing stale-.pyc race in the AuthorMode debug-loop subprocess (PYTHONDONTWRITEBYTECODE) surfaced by the fuller suite.
Rework PydanticAICapabilityProbe into a draft->ground pipeline. After the no-tool drafter, every drafted api_ref is verified against the real source by an MCP-attached agent using a two-tier strategy: an index query, escalating to reference-following (get_source on __init__.py re-exports) when the query is inconclusive. Verified refs fold into evidence via a pure _fold_grounding function; a need whose every ref failed verification is re-drafted with the rejection fed back, bounded by max_grounding_iterations. Targets the two diagnosed capability_evidenced preflight failures: LLM-hallucinated API names, and real re-exported symbols the old discovery agent left unevidenced. The CapabilityProbe protocol, ProbeResult, and capability_projection are unchanged.
ProviderError.__setattr__ rejected all post-_initialized writes, including Python's __traceback__ assignment during exception propagation. Any ProviderError raised inside an @asynccontextmanager (harness.stage() — every mode) crashed with a masking AttributeError instead of surfacing the real error. Allow dunder attributes through the immutability guard; data-field immutability for kind / node_id / tier / cause / attempts is preserved (existing immutability tests still pass). +3 regression tests in test_errors.py: direct __traceback__ set, raise-from chain, async-with propagation (the live-run crash repro).
Drop the procedural / outdated sections that no longer reflect how work happens (and that misled recent design discussions by being cited as authoritative): - Default workflow (which /mol:* skill when) - Commands (pip install / pytest / npm — operational) - After changing the FastAPI backend (procedural recipe) - Adding a New Workflow Task / API Route / UI Renderer (procedural) - Test Organization (tactical layout convention) - Skills (references retired /molexp-* + the /mol:* plugin) Keep only the architecture-related design principles: layer charters, layer details (#1-5), key patterns, packaging, data-type ownership, what-must-never-change. 557 → 440 lines.
Wire the two last no-tool agents in the agent layer through molmcp
so they can verify project API against source instead of guessing from
training data:
(1) PlanMode needs-drafter — '_build_needs_agent' now accepts
'toolsets' / 'tools' / 'output_retries' kwargs; the probe wires the
existing molmcp '_server' into it; '_NEEDS_SYSTEM_PROMPT' mandates
source-verify (search_source / get_signature / get_source) before
emitting any api_ref. '_draft_needs' adds 'async with' and degrades
to empty needs on MCP failure (probe never-raises contract held).
(2) AuthorMode debug-loop repair — new
'_pydanticai/debug_repair.py' exposes 'build_repair_callable(
workspace, model) -> Callable[[str], Awaitable[GeneratedModule]] |
None', returning a closure around a 'pydantic_ai.Agent[None,
GeneratedModule](toolsets=[MCPToolset], output_type=,
output_retries=)'. The closure type is stdlib 'Callable', so
'modes/author/' stays SDK-free. '_apply_targeted_fix' /
'run_task_debug_loop' / 'materialize_plan' accept an optional
'repair=' and route through it when supplied, else fall back to the
legacy 'router.complete_structured' path. AuthorMode gains
'repair_model' / 'workspace' ctor kwargs and a
'_build_repair_callable' helper that lazy-imports
'_pydanticai.debug_repair'.
Strict pydantic-ai native — no new Protocol class, no wrapper class,
no factory module. 'Agent(toolsets=[MCPToolset], ...)' + 'async with
agent' + 'agent.run(...)' is the whole abstraction.
+6 regression tests: drafter source-verify, repair agent escalates on
AttributeError, '_apply_targeted_fix' routes through repair when
supplied and falls back when None.
The drafter and debug-loop repair agents shipped in capability-probe- grounding + mcp-ground-no-tool-agents were calling tools that don't exist in molmcp (`search_source`, `list_symbols`, `get_signature`, `get_source`). The real catalog/browse primitive is `molmcp_find_capability(task=...)` plus `molmcp_describe_symbol` / `molmcp_outline` / `molmcp_search_symbols`. Rewrite both prompts around the browse-then-select pattern the user designed: 'LLM queries the catalog, picks matches' — not 'LLM guesses then verifies.' The drafter MUST call `molmcp_find_capability` for every need before emitting it, accept any returned match whose summary semantically matches the capability (not just name-perfect hits), and try 3+ phrasings before giving up. Repair has a 4-step DISCOVERY PROTOCOL on `AttributeError` / `ImportError` / `NameError`. Live-validated (DeepSeek + real molmcp): the drafter now discovers real symbols like `molpy.tool.polymer.polymer`, `molpy.io.forcefield.xml.read_oplsaa_forcefield`, `molpy.core.script.Script.from_text` at 1.0 confidence — capabilities it was hallucinating before. Tests updated to fake the real molmcp tool names + return shape; examples lean on `Use the molcrafts toolchain — discover the right APIs from the project source` instead of pre-specifying api_refs.
Drafter / grounding / repair prompts no longer hardcode molmcp tool
names. Each prompt describes three abstract tool roles (CATALOG/OUTLINE,
CAPABILITY/SEARCH, DETAIL/LOOKUP) and tells the LLM to match by name
pattern from its own tool list. Drafter now requires a CATALOG/OUTLINE
call as Step 1 before drafting any need; grounding and repair recommend
the same when the missing module is unfamiliar.
Fixes the v4 observation that DeepSeek-chat skipped the catalog and
went straight to semantic search, producing empty drafted needs ~50%
of the time. Mandating the outline read first uses molmcp's own
hierarchical map ("call this first to see what a codebase contains")
as intended.
Phase 01 of agent-mode-stage-pipeline chain — introduces the three
new harness primitives that subsequent phases (PlanMode 02 and the
remaining five modes 03) migrate onto. No concrete mode migrates yet;
existing run() bodies are untouched.
- Stage ABC (harness/stage.py) — plain Generic ABC (not pydantic;
arbitrary_types_allowed=True is forbidden under agent/). Subclasses
pin ``name`` and implement ``run`` as an async generator yielding
AgentEvents + a terminal value. NameOnlyStage is the transitional
placeholder phase 01 wraps mode pipeline declarations with;
deleted at chain end.
- RepairPolicy (harness/repair.py) — frozen pydantic; declarative
"when event kind X fires, rewind to stage Y up to N times, then
route to Z" routing. Per-mode repair *executors* in
modes/{author,run}/repair.py stay; this is the policy.
- execute_pipeline (harness/pipeline.py) — plain async loop walking
a ModePipeline's Stage tuple: brackets each in harness.stage,
forwards events, threads terminal values, honours repairs and an
optional lifecycle_validator. No pydantic_graph, no compiled
graph object.
- ModePipeline (mode.py) — promoted from frozen-pydantic side-band
metadata to a plain class carrying live Stage instances + entry +
repairs + lifecycle_validator. PipelineEdge stays frozen pydantic.
AgentMode.run_pipeline added as the default delegation helper
(AgentMode.run remains @AbstractMethod).
- 6 modes' ``pipeline = ModePipeline(stages=(...))`` declarations
rewrap each name as ``NameOnlyStage(name=...)``. No run() bodies
touched.
Substrate tests: 45 new + backward-compat updates. Full repo suite
1648 passing; ruff + ty clean for touched files; import guard +
no-drift invariants preserved.
…vival-02 follow-up)
…ple (plan-mode-revival-04) Stages now resolve their upstream input BY ARTIFACT KIND at run time (require_latest) — the clean Mode-driven contract; no constructor-id threading (the manual-driver pattern is gone). All 8 planning/validation stages refactored; full_pipeline.py + 12 test files migrated. PlanMode(Mode) declares the 9-stage idea->experiment-plan->WorkflowIR-> BoundWorkflow->molexp.workflow-source sequence (auto-grant approval). examples/harness/plan_mode_live.py runs it end-to-end against REAL DeepSeek — verified live: a NL draft produced a validated 4-task NEMD workflow that compiled to a real Workflow and cleared the gate. Honest prompt grounding (NOT validator-gaming): workflow_ir/bound_workflow/ workflow_source SYSTEM_PROMPTs now explain what a valid DAG / resource-policy / molexp.workflow program is (incl. an API example) so DeepSeek emits valid output. New test_plan_mode.py (7 tests: offline run, ModeResult, provenance user_plan->workflow_source, live-example import safety, examples not collected). 1665 tests pass; ruff + ty clean. ac-012 (live-run docs) parks.
…ess + reorganize examples The word 'Mode' now belongs unambiguously to the harness orchestration concept (harness.Mode / PlanMode). The agent layer's LLM-conversation 'modes' are renamed to 'loops': - AgentMode->AgentLoop, ChatMode->ChatLoop, InteractiveMode->InteractiveLoop (+ *Config); agent/mode.py->loop.py, agent/modes/->agent/loops/. - AgentRunner(mode=)->loop= (+ self.loop); CLI + tests + CLAUDE.md updated. - harness docstrings that contrast against the agent concept now say AgentLoop. - harness.Mode / PlanMode / ModeResult untouched. Examples reorganized to the current architecture: - agent/: chat_loop.py + interactive_loop.py (the 2 shipped agent loops; fixed the stale AgentHarness->AgentRuntime docstring). - harness/: plan_mode_offline.py (NEW — PlanMode end-to-end, deterministic, no key, via StubAgentGateway; prints generated workflow source + lineage + audit) + plan_mode_live.py (real DeepSeek). Deleted the pre-PlanMode full_pipeline.py (raw StageRunner demo, subsumed by PlanMode). - examples/README.md: new Loop/Mode note + Harness Layer section. 1665 tests pass; ruff + ty clean; all three example scripts run.
…ile profiles into molexp.profile
molexp.config is now a live molcfg.Config instance (defined in molexp/__init__.py)
— the process-global, in-code place to register runtime values (notably LLM API
keys), mutated with molcfg-native syntax and never read from the environment:
import molexp
molexp.config["deepseek_api_key"] = "sk-..."
The file-based, per-run profile config (ProfileConfig / MolCfg / load_molcfg /
find_default_config / normalize_profile_name) moves to molexp.profile — a
genuinely separate concern from the global app config.
- molexp/__init__.py: `config: molcfg.Config` instance + docstring; no wrapper verbs.
- rename src/molexp/config/ -> src/molexp/profile/ (drop the old runtime.py);
update consumers (workspace.run, cli.workspace.run, workflow _pydantic_graph).
- agent router _coerce_model_value reads molexp.config.get("deepseek_api_key").
- examples chat_loop / interactive_loop / plan_mode_live use molexp.config[...];
delete the offline stub; README documents the in-code key flow.
- tests/test_config -> tests/test_profile; import-guard prose + CLAUDE.md +
.claude/notes/architecture.md updated (config = instance, profile = file config).
Add a reasoning-delta channel parallel to text deltas so a model's chain-of-thought (DeepSeek reasoner, Claude extended thinking) is no longer silently dropped: - ThinkingDeltaChunk in the Router protocol (agent/router.py), joined into the AgenticChunk union. - _thinking_delta_chunk / _request_stream_chunk translate pydantic-ai ThinkingPart / ThinkingPartDelta (the seam that used to return None). - ThinkingDeltaEvent in the AgentEvent union (16 kinds). - InteractiveLoop maps the chunk to the event. - CLI renders reasoning as a dim/italic collapsible-style stream, distinct from the answer. TDD throughout; 810 agent+harness+cli tests green.
…s (00a) Rebuild the deleted server-side agent session runtime on the new AgentRunner surface as a new molexp.server.agent_runtime subpackage: - AgentTurn / AgentSessionRuntime / AgentSessionRegistry — plain runtime containers owning the background turn task (drains run_events), the collected AgentEvent stream, and per-workspace session lookup. - Registry as a process-singleton accessor (get_agent_runtime) reset on app shutdown via lifespan (cancels + awaits in-flight turns). - Relight create/get/list_sessions in routes/agent.py through the registry; model resolved from in-code molexp.config with a 503 pre-flight when unset; a runner-factory test seam injects a scripted Router so tests need no LLM. Runtime objects never cross response_model. - Import-guard: server.schemas never imports server.agent_runtime. - stream_events / approvals / messages stay 503-stubbed (00b/00c). First link of the agent-live-event-streaming-ui prerequisite chain. TDD throughout; full suite 1682 passing, ruff clean, ty unchanged.
…scoped) A mid-session user message now starts a fresh background turn on the existing Session via AgentSessionRuntime.start_turn (404 unknown session, 409 mid-turn — no interleaving), returning a wire MessageResponse. Re-scoped from the original 00b: the out-of-band approval loop is deferred. Implementation found the agent before_approval gate is dormant (no shipped loop dispatches it) and ReviewPolicy carries no sink, so a server http_ask cannot emit ApprovalRequestedEvent — approvals are blocked on agent-layer work and not needed for the streaming goal. Full suite 1686 passing; ruff clean; ty unchanged (69).
Relight stream_events to deliver a session's live AgentEvent flow over
SSE, lighting up /api/agent-tasks/{id}/events through the existing
delegation:
- agent_runtime/serialize.py: AgentEvent -> 'data: {json}\n\n' frames +
terminal 'done' control frame + single 'error' frame.
- AgentTurn fan-out: a lock makes (append + push-to-subscriber-sinks)
atomic with (snapshot + register), so subscribe() yields replay-then-
tail with no event lost or duplicated at the seam.
- Route: copied molq _SSE_HEADERS, fail-fast 404 before the stream,
done after mode_completed, one error frame on a failed turn.
Deterministic tests drive the StreamingResponse body_iterator directly
(no TestClient race); a gated Router exercises the mid-turn subscribe.
Full suite 1692 passing; ruff clean; ty unchanged (69).
Register the AgentEvent kind-discriminated union as the documented 200 model of the agent-tasks events route (responses=), so app.openapi() emits its 16 member component schemas + discriminator — what link 02's npm run generate:api needs to emit narrowed TS types. Re-export the union from server.schemas (it supersedes the generic SessionEventResponse for the stream; the snapshot shape stays). Add scripts/dump_openapi.py: a deterministic, server-boot-free sorted-key dump to repo-root openapi.json. Re-scoped from the original 01 (the SSE-route relight moved to 00c). Full suite 1694 passing; ruff clean; ty unchanged (69).
…cabulary (02)
The UI now speaks the new AgentEvent kind vocabulary end-to-end:
- EVENT_META rekeyed to the 16 snake_case kinds (incl. thinking_delta /
token_delta); old PascalCase keys removed; unknown kinds still fall
back to the neutral row.
- normalizeStreamFrame(): live SSE AgentEvent frames {kind,timestamp,...}
-> {type:kind, ts:timestamp, payload}; done/waiting -> null. Wired into
AgentViewer es.onmessage so snapshot + stream key uniformly on event.type.
- groupEventsIntoTurns splits turns on mode_started (no user_message_received
in the union); isResultEvent -> mode_completed/plan_emitted.
- derivePendingUserRequest -> trailing clarification_required (no request_id).
- AgentViewer event.type conditionals + PlanCard/approval/tool blocks rekeyed.
- ApiSessionEvent stays = SessionEventResponse (type carries the snake_case
kind) — deliberately NOT repointed to the generated union (avoids the
agent_tasks review-sync cascade).
228 rstest pass; tsc/biome add no new errors over baseline. 3 ui_runtime
criteria parked for /mol:web.
Render the agent transcript as a live stream on top of 02's vocabulary:
- foldStreamedTurn(events) -> {answer, thinking, toolCalls}: a pure
reducer concatenating consecutive token_delta into the answer and
thinking_delta into a separate reasoning string, and collapsing each
tool_call_started/completed pair into one ToolCallState (FIFO by
tool_name); answer falls back to mode_completed.text when no tokens
streamed.
- ThinkingBlock: default-collapsed, dim/italic, 💭, 'Thinking…' while
streaming. ToolCallRow: started (spinner ⚙) -> completed (✓/✗ + summary)
in place.
- AgentViewer TurnCard memoizes the fold over the turn's events and renders
the thinking block, tool-call rows, and the token-by-token answer; 02's
batch event rows are untouched.
6 reducer unit tests; 234 rstest pass; tsc/biome add no new errors in
changed files. 4 ui_runtime criteria parked for /mol:web.
Make dev:mock exercise the live streaming agent UI so /mol:web (and manual inspection) can observe it: - agent.ts: the /agent-tasks/:id/events handler now streams a timed new snake_case AgentEvent sequence (mode_started → thinking_delta×2 → tool_call_started/completed → token_delta×4 → mode_completed → done), ~250ms/frame; the delayed snapshot event uses mode_completed/plan_emitted. - agent_admin.ts: default mock provider apiKeySet=true so the new-goal form is enabled (the real backend gate doesn't apply to the mock). - db/index.ts: getAgentSession resolves by taskId as well as sessionId (routes look up by the /agent-tasks/:taskId param) — fixes a 404 on task detail + event stream. Verified via /mol:web: 03-stream's 4 ui_runtime criteria pass.
…ntic-ai) The hooks/approval extension seam was wired at both ends but never energized: zero hooks.dispatch() calls anywhere — no shipped loop fires any of the 5 hook points, and the InteractiveLoop's read-only tools need no approval. It also reinvents what pydantic-ai 1.97 provides natively (DeferredToolRequests / ApprovalRequired / ToolApproved for approval, event_stream_handler for hooks). Delete it (behavior-preserving): - remove agent/hooks.py + agent/review.py (HookRegistry/HookPoint/ ReviewDecision/ReviewPolicy/cli_ask) - AgentRuntime bundle drops the hooks field (session+router+execution_env) - AgentRunner drops the approval= param + _build_hooks - agent.__all__ shrinks to the 5 stable names; CLI _make_runner drops approval=cli_ask - CLAUDE.md: strip the perishable approval-primitive / hooks enumerations (CLAUDE.md should carry abstract conventions, not stale symbol lists) Out of scope: the server/UI plan-review surface (separate, stale-in-its- own-way; its own follow-up). 1687 tests pass; ruff/ty green (no new).
The plan-review / approval feature was old-architecture cruft, never fed by the live agent flow: the server runs InteractiveLoop (token/thinking/ tool/mode only, never plan_emitted/approval_requested), and the review- sync scanned events for the non-existent PascalCase "PlanCreatedEvent", so reviews were never created, /reviews always returned empty (no UI consumer, no test), and respond_approval/respond_plan were 503 stubs. Delete it (behavior-preserving): - server: review_store.py + reviews.py (whole), review-sync helpers + approve/plan-decision routes in agent_tasks.py, respond_approval/ respond_plan in agent.py, and the dead request/response schemas (Approval/Plan/Review*). - ui: PlanCard.tsx + the actionable approval block + onApprovalRespond/ onPlanResolved threading in AgentViewer; respondApproval/respondPlan in api.ts; the reviews + approve/plan-decision MSW handlers. - generated client: drop ReviewsService + the review/approval models + the AgentTasks approve/plan-decision ops. Kept: the plan_emitted/approval_requested AgentEvent kinds (real harness events) + their EVENT_META labels (neutral row render); harness PlanMode/ ApprovalGate untouched. If plan-review UX is wanted later it gets rebuilt fresh on the harness path. Server 1685 pytest + UI 234 rstest pass; ruff/ty/biome/tsc no new errors; dead-ref sweep zero.
A non-standard molecular dataset (reference: a QM9 download via molnex)
can carry its own loader as a same-stem `.py` sidecar that defines exactly
one concrete `molpy.io.BaseTrajectoryReader` subclass. molexp discovers the
sidecar without importing it, and only on an explicit preview request imports
it under a private (non-`__main__`) module name, instantiates the single
reader with the dataset path, takes a host-owned `islice` of frames, and
renders them.
Server:
- preview.py: discover_reader_sidecar (existence-only, no-import),
load_sidecar_reader (private-name importer + exactly-one-subclass),
preview_frames (host islice cap), frames_to_extxyz, snapshot_reader
(headless molvis), asset_has_sidecar.
- GET /api/assets/{asset_id}/preview?format=frames|png.
- Typed 4xx exceptions (404 missing, 422 zero/ambiguous/broken) — never 500.
- has_preview_sidecar flag on the asset listing (existence-only signal).
UI:
- Thread hasPreviewSidecar through FileMatchContext; molvis FileTypeContribution
matches sidecar-backed datasets that hit no extension pattern.
Tests: ac-001..009 (ac-008 QM9 reference reader skip-gated; ac-005 PNG
skip-gated on molvis; ac-010 live UI wiring deferred to /mol:web).
Previews are strictly index-driven: they operate on a *registered* dataset asset (resolved by asset_id through the catalog). There is no workspace file-tree scanning and no path-based preview of unregistered files. The sidecar is not an asset — just the same-stem .py sibling of the registered asset's resolved path. - Rename discover_reader_sidecar -> resolve_sidecar (it resolves a known registered path's sidecar; it does not scan/auto-discover). Clarify the module/contract docstrings accordingly. - workspace: DataAssetLibrary.register_in_place — register an existing workspace file as a DataAsset without copying into a payload dir, so the original stem (and its sidecar sibling) is preserved. New "reference" ImportAction. - server: POST /api/assets/data/register registers an in-place file; rejects paths outside the workspace (400). End-to-end register -> preview test (also guards the molpy XYZ-writer atom-count fix at the molexp boundary).
Wires the registered dataset asset's sidecar flag from the catalog index
through the workspace file tree into a molvis "Preview" tab.
- server: list_workspace_files (?include=catalog) annotates each registered
asset node with hasPreviewSidecar (index-gated; resolve_sidecar on the
asset's resolved path). No file-tree scanning of unregistered files.
- ui: thread assetId + hasPreviewSidecar from the file node -> selection ->
getPluginForFile/canHandle -> FilePreviewContentProps.
- ui: molvis FilePreviewPlugin "molvis:dataset-preview" (canHandle keys off
hasPreviewSidecar); MolvisDatasetPreview mounts molvis-core and renders the
frames fetched from GET /api/assets/{id}/preview?format=frames (loaded as
preview.xyz). Shows a register-first hint when the file is not a registered
asset.
Tests: server file-tree flag test; rstest gating on hasPreviewSidecar.
Full suite 1710 passed; UI typecheck/biome clean; production build OK.
ac-010 (live in-browser 3D render) still needs a browser session to verify.
… URL
Selection round-trips through the URL (?file=&fileKind=), which dropped the
asset id and the sidecar flag — so the molvis Preview tab never lit up even
though LeftPanel set them on the selection. Serialize/parse both in
useNavigationState. Verified live: selecting a sidecar-backed dataset now
shows Edit|Preview and molvis renders the trajectory from
/api/assets/{id}/preview?format=frames.
Replace the `workspace <TARGET>` god-group with a flat two-level tree. All
verbs (run/serve/monitor/explore/info/exec/shell/sync/push/pull) and noun
groups (project/experiment/runs/asset/target/session/config/mcp) register
directly on the top-level app. TARGET moves from a group positional to a
per-command `-t/--target` option (default cwd) via the new
`cli/_target.resolve_workspace_target` (shared resolver wrapping
workspace.target). `@name` now resolves against the cwd workspace registry.
Renames: exec-cmd→exec, upload→push, download→pull. Dedups init/target/runs
to one home each (drops the resources `target` duplicate; keeps the tested
top-level `target_cmd`). `run`'s old `--target` (compute target) →
`--compute-target`, and `--time` drops its `-t` short flag so `-t` is the
workspace target everywhere.
Implements specs cli-redesign-0{1,2,3,4}. 113 cli tests pass; ruff + ty clean
on src/molexp/cli/.
…flat-tree redesign)
- CI lint job now runs the exact hook set from .pre-commit-config.yaml instead of duplicated ruff/ty steps that could drift. - ty stays advisory in BOTH (codebase has ~331 ty diagnostics; was already continue-on-error in CI — now the pre-commit hook matches via 'ty check || true'). - Exclude generated OpenAPI client (ui/src/api/generated/) from all hooks. - Apply the resulting ruff-format/eof fixes (test_help_surface.py, moko.svg).
ci: align CI lint with pre-commit config
docs: prefer shadcn/ui for frontend components
Roy-Kid
added a commit
to Roy-Kid/molab
that referenced
this pull request
Jul 4, 2026
…-execution-01) Slice 01/03 of the P2.1 guarded-execution chain (integration.md §6.1, §8.3). New molexp.harness.actions subpackage — the coordination-layer "Act" seam a granted ChangeProposal will dispatch through: - ProposalExecutor.dispatch(ctx, proposal) — resolves the handler for the proposal's HighRiskOp and records the attempt. - ChangeActionHandler Protocol + ChangeActionRegistry (HighRiskOp → handler, modeled on InMemoryCapabilityRegistry). - assert_within_affected_scope — the §8.2 binding-scope guard. - ProposalActionRecorder — traceable tool_called/tool_completed/tool_failed events (reuses the existing EventType values — NO widening), each carrying proposal_id + high_risk_op (invariant MolCrafts#7). Contracts: unhandled op → UnhandledHighRiskOpError (loud, no event, §10 MolCrafts#10); handler success → executed; handler exception (incl. OutOfAffectedScopeError) → recorded status=failed + tool_failed, not raised (§8.3). No schema change (ProposalOutcome already carries executed/failed). Stub handler lives in tests — no concrete handler ships in src/. Gate untouched (slice 03). Full suite 2695 passed; import-guard green (import molexp.harness stays free of pydantic_ai/pydantic_graph).
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
master is a stale snapshot (79 commits behind dev) whose CI fails (ModuleNotFoundError: molexp.cli.run_cmd — a test references a since-renamed module). dev is green. Fast-forward master to dev to fix master CI and refresh the aggregate mirror.