Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ real text-agent equivalent exists.
| `torch.optim` | `hytorch.optim` |
| learning rate | mutation temperature `temp` |
| applied parameter update | committed Git mutation |
| `model.state_dict()` | `model.state_dir()` |
| `torch.save(...)` | `hytorch.save(...)` |
| `torch.load(...)` | `hytorch.load(...)` |
| `model.load_state_dict(...)` | `model.load_state_dir(...)` |

`Module.__setattr__` registers Parameters and child Modules. Registration owns
state; calls in `forward()` dynamically define topology. `model.parameters()`
Expand Down Expand Up @@ -75,6 +79,12 @@ candidate model branch.
`mn.init.DEFAULT_PRIORS`. It can later contain arbitrary code, tools, examples,
and data.

Model checkpoint syntax follows PyTorch with a directory-native representation:
`hytorch.save(model.state_dir(), path)` and
`model.load_state_dir(hytorch.load(path))`. A StateDir fixes one canonical model
commit and preserves the complete model Git history. It excludes feedback,
sessions, temporary node trees, and unpromoted optimizer candidates.

Forward returns the complete committed statespace, never a special answer file.
Do not inject Space contents into the agent prompt. Mount complete directory
trees. `zero_feed()` clears accumulated feedback and discards an unpromoted
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to HyTorch will be recorded in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
HyTorch uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- PyTorch-shaped, directory-native model checkpoints with `state_dir()`,
`hytorch.save()`, `hytorch.load()`, and `load_state_dir()`.

## [0.1.0] - 2026-08-05

### Added
Expand Down
4 changes: 3 additions & 1 deletion GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
| parameter delta | candidate workspace mutation | Proposed change made during backward |
| updated parameter storage | promoted global Git commit | Canonical model generation after `step()` |
| saved forward activations | statespace commit and harness session | Context resumed during backward |
| `state_dict()` | model workspace Git repository | Serialization target; API not yet implemented |
| `state_dict()` | `state_dir()` | Immutable handle to the canonical model-state revision |
| `torch.save(model.state_dict(), path)` | `hytorch.save(model.state_dir(), path)` | Save complete parameter state and canonical history |
| `load_state_dict(torch.load(path))` | `load_state_dir(hytorch.load(path))` | Validate and restore registered workspaces |

The canonical training form is:

Expand Down
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,39 @@ previous iteration. `backward()` resumes the agents and creates candidate
workspace changes. `step()` promotes all completed changes as one new model
generation.

### Save and load model state

Save the complete canonical model state as a self-contained Git directory:

```python
hytorch.save(model.state_dir(), "research-network")
```

Create the same model structure, then load the saved workspaces:

```python
restored = ResearchNetwork().to("pi")
result = restored.load_state_dir(hytorch.load("research-network"))
```

This follows the PyTorch `state_dict()` pattern with a directory-native state:

```python
# PyTorch
torch.save(model.state_dict(), "model.pth")
model.load_state_dict(torch.load("model.pth", weights_only=True))

# HyTorch
hytorch.save(model.state_dir(), "model-state")
model.load_state_dir(hytorch.load("model-state"))
```

A `StateDir` identifies one immutable model commit. The saved directory
contains `MODEL.json`, every registered workspace, and the canonical model Git
history. Loading is strict by default. Pass `strict=False` to permit missing or
unexpected workspace keys with compatible shapes. The save destination must
not already exist.

## PyTorch-shaped composition

HyTorch follows PyTorch syntax and ownership where a direct agent equivalent
Expand Down Expand Up @@ -215,8 +248,8 @@ environments and review agent-created changes before production use.

Version 0.1.0 includes Spaces, Parameters, dynamic Module graphs, dense Linear
layers, directional backward feedback, atomic DFM optimizer generations, and
the Dockerized Pi harness. It does not yet implement `state_dict()`. The
`codex` and `claude-code` harnesses are reserved but unavailable.
the Dockerized Pi harness. The `codex` and `claude-code` harnesses are reserved
but unavailable.

## Resources

Expand Down
34 changes: 34 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,37 @@ Feedback is transient text and does not use a third Git repository. Git gives
Spaces and model generations stable identity, ancestry, diffs, audit history,
and atomic promotion.

## Model state directories

HyTorch serializes model state as a directory because each Parameter element is
already a complete directory. The public form follows PyTorch checkpoint
syntax:

```python
hytorch.save(model.state_dir(), path)
model.load_state_dir(hytorch.load(path))
```

`model.state_dir()` returns a `StateDir` fixed to the canonical model commit at
the time of the call. It does not include an unpromoted DFM candidate.
`hytorch.save()` creates a self-contained Git directory at that exact commit.
The saved state contains `MODEL.json`, all registered workspace directories,
and the canonical model history. It excludes feedback, active harness sessions,
temporary node trees, and optimizer candidates.

`hytorch.load()` validates the repository root, committed `MODEL.json`, format,
and workspace paths. It returns a `StateDir`; it does not modify a model.
`model.load_state_dir()` copies matching workspaces into an initialized model
and records one canonical load commit. The default `strict=True` requires the
saved and destination workspace keys to match exactly. `strict=False` permits
missing and unexpected keys, but shape and module-type mismatches remain
errors. The return value reports missing and unexpected keys in the same style
as PyTorch's `load_state_dict()`.

State capture and load require a clean canonical model worktree. Loading while
an optimizer candidate is pending is an error. Validation must finish before
HyTorch changes any destination workspace.

## Required invariants

1. One output feature executes one agent.
Expand All @@ -264,3 +295,6 @@ and atomic promotion.
11. Backward closes each resumed forward session.
12. Only `optimizer.step()` promotes candidate workspace commits.
13. `zero_feed()` never changes committed canonical workspace history.
14. A StateDir identifies one committed, immutable model generation.
15. Saved model state never includes transient feedback, sessions, or candidates.
16. A failed state load leaves every destination workspace unchanged.
4 changes: 4 additions & 0 deletions hytorch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ._random import manual_seed
from .backward import Loss, Report, WorkspaceRevision
from .space import Space, SpaceBatch, space
from .state_dir import StateDir, load, save

__version__ = "0.1.0"

Expand All @@ -17,13 +18,16 @@
"Report",
"Space",
"SpaceBatch",
"StateDir",
"WorkspaceRevision",
"__version__",
"harness",
"inference_mode",
"is_inference_mode_enabled",
"load",
"manual_seed",
"mn",
"optim",
"save",
"space",
]
11 changes: 11 additions & 0 deletions hytorch/_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ def resolve(self, ref: str) -> str:
except GitError as exc:
raise GitError(f"resolve git ref {ref!r}: {exc}") from exc

def read_file(self, commit: str, path: str) -> bytes:
"""Read one file from a committed tree."""
result = subprocess.run(
["git", "-C", self.root, "show", f"{commit}:{path}"],
capture_output=True,
check=False,
)
if result.returncode != 0:
raise GitError(result.stderr.decode(errors="replace").strip())
return result.stdout

def current_branch(self) -> str:
branch = _run(self.root, ["symbolic-ref", "--quiet", "--short", "HEAD"])
if not branch:
Expand Down
18 changes: 18 additions & 0 deletions hytorch/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ def parameters(self, recurse: bool = True):
self._ensure_parameter_store()
return (parameter for _, parameter in self.named_parameters(recurse=recurse))

def state_dir(self):
"""Return an immutable handle to the canonical model-state revision."""
from .state_dir import StateDir

store = self._ensure_parameter_store()
if not store.repo.is_clean():
raise RuntimeError(
"hytorch.mn.Module.state_dir: model state has uncommitted changes"
)
return StateDir(store.root, store.repo.resolve("HEAD"))

def load_state_dir(self, state_dir, strict: bool = True):
"""Copy a StateDir into this Module and its descendants."""
from .state_dir import load_module_state

return load_module_state(self, state_dir, strict)

def apply(self, fn):
for child in self.children():
child.apply(fn)
Expand Down Expand Up @@ -173,6 +190,7 @@ def _ensure_parameter_store(self) -> ParameterStore:
"parameters": {
parameter_name: {
"shape": list(parameter.shape),
"input_features": parameter.input_features,
"workspaces": [
view.relative_path for view in parameter.views()
],
Expand Down
Loading