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
2 changes: 1 addition & 1 deletion .data/snapshots/state_AgentA.json.meta
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"created_at": "1980-01-01T00:00:00Z", "schema_version": "v1"}
{"created_at": "2025-10-10T08:23:58Z", "schema_version": "v1"}
2 changes: 1 addition & 1 deletion .data/snapshots/state_Ambrose.json.meta
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"created_at": "1980-01-01T00:00:00Z", "schema_version": "v1"}
{"created_at": "2025-10-10T08:23:57Z", "schema_version": "v1"}
2 changes: 1 addition & 1 deletion .data/snapshots/state_agent.json.meta
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"created_at": "1980-01-01T00:00:00Z", "schema_version": "v1"}
{"created_at": "2025-10-10T08:23:55Z", "schema_version": "v1"}
2 changes: 1 addition & 1 deletion .data/snapshots/state_smoke.json.meta
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"created_at": "1980-01-01T00:00:00Z", "schema_version": "v1"}
{"created_at": "2025-10-10T08:23:58Z", "schema_version": "v1"}
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ env:
PYTHONUTF8: "1"
PYTHONHASHSEED: "0"
LC_ALL: C.UTF-8
LANG: C.UTF-8
SOURCE_DATE_EPOCH: "315532800"

jobs:
Expand Down Expand Up @@ -98,6 +99,8 @@ jobs:
pip install -e .[dev,test]
# Ensure PyYAML is present (not included in extras)
pip install pyyaml
# Ensure deterministic completions output
python -m pip install "shtab==1.7.1"

- name: Sanity imports
continue-on-error: true
Expand Down Expand Up @@ -157,6 +160,8 @@ jobs:
CI: "true"
PYTHONHASHSEED: "0"
COLUMNS: "80"
LINES: "25"
LANG: "C.UTF-8"
run: |
set -euo pipefail
pytest -q -m "not manual"
Expand Down Expand Up @@ -199,13 +204,17 @@ jobs:
pip install -e ".[test,dev]"
# Fallback in case extras not defined on forks
pip install pytest || true
# Ensure deterministic completions output
python -m pip install "shtab==1.7.1"

- name: Run CLI help stability tests
env:
CLEMATIS_NETWORK_BAN: "1"
CI: "true"
PYTHONHASHSEED: "0"
COLUMNS: "80"
LINES: "25"
LANG: "C.UTF-8"
run: |
pytest -q tests/cli/test_help_stability.py

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/cli_smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ jobs:
env:
CI: "true"
TZ: "UTC"
LANG: "C.UTF-8"
PYTHONUTF8: "1"
PYTHONHASHSEED: "0"
LC_ALL: "C.UTF-8"
SOURCE_DATE_EPOCH: "315532800"
CLEMATIS_NETWORK_BAN: "1"
PYTHONDONTWRITEBYTECODE: "1"
COLUMNS: "80"
LINES: "25"

steps:
- name: Checkout
Expand Down Expand Up @@ -67,6 +69,8 @@ jobs:
else
python -m pip install pytest hypothesis
fi
# Ensure deterministic completions output
python -m pip install "shtab==1.7.1"

- name: Pre-check (version/help)
shell: bash
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ site/
# Caches & scratch
.cache/
.ipynb_checkpoints/
.lancedb/
lancedb/

# Logs & artifacts (non-identity; keep tests' goldens intact)
logs/
Expand Down
2 changes: 1 addition & 1 deletion .logs/apply.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "applied": 0, "clamps": 0, "version_etag": "46", "snapshot": "./.data/snapshots/state_AgentA.json", "cache_invalidations": 0, "ms": 0.785}
{"turn": "demo-1", "agent": "AgentA", "applied": 0, "clamps": 0, "version_etag": "46", "snapshot": "./.data/snapshots/state_AgentA.json", "cache_invalidations": 0, "ms": 0.777}
2 changes: 1 addition & 1 deletion .logs/t1.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "pops": 5, "iters": 1, "propagations": 3, "radius_cap_hits": 0, "layer_cap_hits": 0, "node_budget_hits": 0, "max_delta": 1.0, "graphs_touched": 1, "cache_hits": 0, "cache_misses": 1, "cache_used": false, "cache_enabled": true, "ms": 0.312, "now": "2025-10-10T02:07:37.100667+00:00"}
{"turn": "demo-1", "agent": "AgentA", "pops": 5, "iters": 1, "propagations": 3, "radius_cap_hits": 0, "layer_cap_hits": 0, "node_budget_hits": 0, "max_delta": 1.0, "graphs_touched": 1, "cache_hits": 0, "cache_misses": 1, "cache_used": false, "cache_enabled": true, "ms": 0.278, "now": "2025-10-10T08:23:58.151083+00:00"}
2 changes: 1 addition & 1 deletion .logs/t2.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "tier_sequence": ["exact_semantic", "cluster_semantic", "archive"], "k_returned": 0, "k_used": 0, "k_residual": 0, "sim_stats": {"mean": 0.0, "max": 0.0}, "score_stats": {"mean": 0.0, "max": 0.0}, "owner_scope": "any", "caps": {"residual_cap": 32}, "cache_enabled": true, "cache_used": true, "cache_hits": 0, "cache_misses": 2, "backend": "inmemory", "backend_fallback": false, "hybrid_used": false, "cache_hit": false, "cache_size": 1, "ms": 0.185, "now": "2025-10-10T02:07:37.100667+00:00"}
{"turn": "demo-1", "agent": "AgentA", "tier_sequence": ["exact_semantic", "cluster_semantic", "archive"], "k_returned": 0, "k_used": 0, "k_residual": 0, "sim_stats": {"mean": 0.0, "max": 0.0}, "score_stats": {"mean": 0.0, "max": 0.0}, "owner_scope": "any", "caps": {"residual_cap": 32}, "cache_enabled": true, "cache_used": true, "cache_hits": 0, "cache_misses": 2, "backend": "inmemory", "backend_fallback": false, "hybrid_used": false, "cache_hit": false, "cache_size": 1, "ms": 0.145, "now": "2025-10-10T08:23:58.151083+00:00"}
2 changes: 1 addition & 1 deletion .logs/t3.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "backend": "rulebased", "ops_counts": {"Speak": 1, "RequestRetrieve": 1}, "requested_retrieve": true, "rag_used": true, "ms_plan": 0.042, "ms_rag": 0.066, "ms_speak": 0.034, "now": "2025-10-10T02:07:37.100667+00:00"}
{"turn": "demo-1", "agent": "AgentA", "backend": "rulebased", "ops_counts": {"Speak": 1, "RequestRetrieve": 1}, "requested_retrieve": true, "rag_used": true, "ms_plan": 0.032, "ms_rag": 0.055, "ms_speak": 0.036, "now": "2025-10-10T08:23:58.151083+00:00"}
2 changes: 1 addition & 1 deletion .logs/t3_dialogue.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "tokens": 6, "truncated": false, "style_prefix_used": false, "snippet_count": 0, "ms": 0.034, "backend": "rulebased", "now": "2025-10-10T02:07:37.100667+00:00"}
{"turn": "demo-1", "agent": "AgentA", "tokens": 7, "truncated": false, "style_prefix_used": false, "snippet_count": 0, "ms": 0.036, "backend": "rulebased", "now": "2025-10-10T08:23:58.151083+00:00"}
2 changes: 1 addition & 1 deletion .logs/t3_plan.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "policy_backend": "rulebased", "backend": "rulebased", "ops_counts": {"Speak": 1, "RequestRetrieve": 1}, "requested_retrieve": true, "rag_used": true, "reflection": false, "ms_deliberate": 0.042, "ms_rag": 0.066, "now": "2025-10-10T02:07:37.100667+00:00"}
{"turn": "demo-1", "agent": "AgentA", "policy_backend": "rulebased", "backend": "rulebased", "ops_counts": {"Speak": 1, "RequestRetrieve": 1}, "requested_retrieve": true, "rag_used": true, "reflection": false, "ms_deliberate": 0.032, "ms_rag": 0.055, "now": "2025-10-10T08:23:58.151083+00:00"}
2 changes: 1 addition & 1 deletion .logs/t4.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "counts": {"input": 0, "after_cooldown": 0, "after_novelty": 0, "after_l2": 0, "approved": 0, "dropped_tail": 0}, "clamps": {"novelty_clamped": 0, "l2_scale": 1.0}, "cooldowns": {"blocked_ops": 0}, "caps": {"delta_norm_cap_l2": 1.5, "novelty_cap_per_node": 0.3, "churn_cap_edges": 64}, "approved": 0, "rejected": 0, "reasons": [], "ms": 0.009, "now": "2025-10-10T02:07:37.100667+00:00"}
{"turn": "demo-1", "agent": "AgentA", "counts": {"input": 0, "after_cooldown": 0, "after_novelty": 0, "after_l2": 0, "approved": 0, "dropped_tail": 0}, "clamps": {"novelty_clamped": 0, "l2_scale": 1.0}, "cooldowns": {"blocked_ops": 0}, "caps": {"delta_norm_cap_l2": 1.5, "novelty_cap_per_node": 0.3, "churn_cap_edges": 64}, "approved": 0, "rejected": 0, "reasons": [], "ms": 0.006, "now": "2025-10-10T08:23:58.151083+00:00"}
2 changes: 1 addition & 1 deletion .logs/turn.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"turn": "demo-1", "agent": "AgentA", "durations_ms": {"t1": 0.312, "t2": 0.185, "t4": 0.009, "apply": 0.785, "total": 3.189}, "t1": {"pops": 5, "iters": 1, "graphs_touched": 1}, "t2": {"k_returned": 0, "k_used": 0, "cache_hit": false}, "t4": {"approved": 0, "rejected": 0}, "now": "2025-10-10T02:07:37.100667+00:00"}
{"turn": "demo-1", "agent": "AgentA", "durations_ms": {"t1": 0.278, "t2": 0.145, "t4": 0.006, "apply": 0.777, "total": 2.955}, "t1": {"pops": 5, "iters": 1, "graphs_touched": 1}, "t2": {"k_returned": 0, "k_used": 0, "cache_hit": false}, "t4": {"approved": 0, "rejected": 0}, "now": "2025-10-10T08:23:58.151083+00:00"}
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Fixed
- **T2 / LanceDB:** exact-semantic recency filters now honour the orchestrator-provided `hints["now"]` timestamp before falling back to wall-clock UTC, keeping Lance replay results aligned with the in-memory backend during deterministic replays.

### Docs
- Documented the LanceDB recency behaviour and the console `--now-ms` requirement for identity runs (README, operator guide, `docs/m3/lance.md`).

## [0.10.3] - 2025-10-09

### M14 — Examples & fixtures (viewer/console)
Expand Down
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ recursive-include docs *.md
# Frontend viewer: ship prebuilt static assets in sdists
recursive-include clematis/frontend/dist *

# Prompt templates for demos/LLM scaffolding
recursive-include configs/prompts *.txt

# Do not ship repo-level frontend sources (TS/Node dev tree)
prune frontend

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

Clematis is a deterministic, turn‑based scaffold for agential AI. It models agents with concept graphs and tiered reasoning (T1→T4), uses small LLMs where needed, and keeps runtime behavior reproducible (no hidden network calls in tests/CI).

> **Status:** **v0.10.0** (2025‑10‑08) — **M13 Hardening & Freeze (frozen)**. See **[docs/m13/overview.md](docs/m13/overview.md)**. **M12 skipped** for v3. **M11 complete** ✅ (HS1/GEL substrate). Defaults unchanged; all GEL paths are **gated and OFF by default**; identity path preserved. M10 remains complete; M9 deterministic parallelism remains flag‑gated and OFF by default.
> **Status:** **v0.10.3** (2025‑10‑09) — v3 remains frozen after **M13 Hardening & Freeze**; recent 0.10.x updates are docs/examples only. See **[docs/m13/overview.md](docs/m13/overview.md)** for the locked surface. **M12 skipped** for v3. **M11 complete** ✅ (HS1/GEL substrate). Defaults unchanged; all GEL paths are **gated and OFF by default**; identity path preserved. M10 remains complete; M9 deterministic parallelism remains flag‑gated and OFF by default.
>
> **License:** Apache‑2.0 — see [LICENSE](./LICENSE) & [NOTICE](./NOTICE).
> **Support matrix:** Python **3.11–3.13**; Ubuntu, macOS, Windows. Cross‑OS identity and reproducible builds (SBOM/SLSA) enforced in CI.
> **Changelog:** see [CHANGELOG.MD](CHANGELOG.MD) for **v0.10.1**.
> **Changelog:** see [CHANGELOG.MD](CHANGELOG.MD) for **v0.10.3**.
>
> **M13 — Hardening & Freeze (v3):** See **[docs/m13/overview.md](docs/m13/overview.md)**.
> **M14 — Viewer & Console (docs):** See **[docs/m14/frontend.md](docs/m14/frontend.md)**.
Expand Down Expand Up @@ -82,6 +82,7 @@ TZ=UTC PYTHONHASHSEED=0 SOURCE_DATE_EPOCH=315532800 CLEMATIS_NETWORK_BAN=1 \
python -m clematis console -- step --now-ms 315532800000 --out /tmp/run.json
python -m clematis console -- compare --a /tmp/run.json --b /tmp/run.json
```
> ⚖️ Identity tip: Passing `--now-ms` (or exporting `SOURCE_DATE_EPOCH`) keeps T2’s `exact_recent_days` window aligned across the in-memory and LanceDB backends when replaying bundles or comparing logs.

Local reproducibility + offline checks for the viewer:

Expand Down
70 changes: 67 additions & 3 deletions clematis/adapters/embeddings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from __future__ import annotations
from typing import List
from typing import List, Optional
import numpy as np
from numpy.typing import NDArray
import hashlib
import os
import logging

logger = logging.getLogger(__name__)


class _DevDummyEmbeddingAdapter:
Expand Down Expand Up @@ -48,7 +52,67 @@ def encode(self, texts: List[str]) -> List[NDArray[np.float32]]:
return vecs


# Alias for clarity with planned BGE usage in T2
BGEAdapter = DeterministicEmbeddingAdapter
# Alias for clarity with planned BGE usage in T2 (deterministic fallback)
class BGEAdapter:
"""
Wrapper that attempts to load the real BGE v1.5 encoder when available.

By default we keep the deterministic adapter for reproducibility. Set the
environment variable ``CLEMATIS_USE_REAL_BGE=1`` (or pass ``use_real=True``)
to enable the SentenceTransformer-backed encoder.
"""

_ENV_FLAG = "CLEMATIS_USE_REAL_BGE"

def __init__(
self,
dim: int = 32,
normalize: bool = True,
*,
use_real: Optional[bool] = None,
model_name: str = "BAAI/bge-base-en-v1.5",
device: Optional[str] = None,
) -> None:
flag = use_real
if flag is None:
flag = os.getenv(self._ENV_FLAG, "").strip().lower() in {"1", "true", "yes", "on"}

self.normalize = bool(normalize)
self._stub = DeterministicEmbeddingAdapter(dim=dim, normalize=normalize)
self._model = None
self._use_real = bool(flag)
self.dim = self._stub.dim

if self._use_real:
try:
from sentence_transformers import SentenceTransformer # type: ignore

logger.info("Loading real BGE encoder '%s' (device=%s)", model_name, device or "auto")
self._model = SentenceTransformer(model_name, device=device)
try:
self.dim = int(self._model.get_sentence_embedding_dimension()) # type: ignore[attr-defined]
except Exception:
# Fallback: infer dim from a dummy encode
sample = self._model.encode(["probe"], convert_to_numpy=True)
self.dim = int(np.asarray(sample[0]).shape[-1])
except Exception as exc:
logger.warning(
"Falling back to deterministic BGE adapter (failed to load '%s': %s)",
model_name,
exc,
)
self._model = None
self._use_real = False

def encode(self, texts: List[str]) -> List[NDArray[np.float32]]:
if self._model is not None:
vectors = self._model.encode(
texts,
convert_to_numpy=True,
normalize_embeddings=self.normalize,
)
return [np.asarray(vec, dtype=np.float32) for vec in vectors]
return self._stub.encode(texts)


__all__ = ["DeterministicEmbeddingAdapter", "BGEAdapter"]
26 changes: 26 additions & 0 deletions clematis/adapters/ollama_transport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# clematis/adapters/ollama_transport.py
from __future__ import annotations
import json
import urllib.request

def generate_with_ollama(prompt: str, *, model: str, max_tokens: int, temperature: float, timeout_s: float) -> str:
body = {
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": temperature,
"num_predict": max(0, int(max_tokens)),
},
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
"http://localhost:11434/api/generate",
data=data,
headers={"content-type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
payload = json.loads(resp.read().decode("utf-8"))
# /api/generate returns {"response": "...", ...}
return (payload.get("response") or "").strip()
39 changes: 39 additions & 0 deletions clematis/cli/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

from typing import Optional

# Reuse the implementation that lives in scripts/chat.py
try: # prefer packaged location
from clematis.scripts.chat import main as _chat_main
except Exception: # dev fallback when running from repo root
try:
from scripts.chat import main as _chat_main
except Exception as e: # helpful error if neither path is available
raise ModuleNotFoundError(
"Unable to import chat implementation. Expected 'clematis.scripts.chat' "
"when installed, or 'scripts.chat' in a source checkout. If you're in 'dist/', "
"run from the repo root or install the package."
) from e


def register(subparsers) -> None:
"""
Register 'chat' as a pass-through subcommand so `python -m clematis chat` works.
"""
p = subparsers.add_parser(
"chat",
help="Interactive chat loop with optional LLM backend",
)

def _run(ns) -> int:
argv = list(getattr(ns, "args", []))
if argv and argv[0] == "--":
argv = argv[1:]
return _chat_main(argv)

p.set_defaults(func=_run)


def main(argv: Optional[list[str]] = None) -> int:
"""Direct entrypoint allowing `python -m clematis.chat` style execution."""
return _chat_main(argv or [])
2 changes: 1 addition & 1 deletion clematis/cli/export_logs_for_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _run(ns: argparse.Namespace) -> int:
def register(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
help_text = "Export logs + latest snapshot into a JSON bundle (delegates to scripts/)"
epilog = (
"Arguments after an optional '--' are forwarded verbatim to "
"Arguments after an optional '--' are forwarded verbatim to\n"
"scripts/export_logs_for_frontend.py"
)
p = subparsers.add_parser(
Expand Down
2 changes: 2 additions & 0 deletions clematis/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
validate,
export_logs_for_frontend,
console,
chat,
)
from ._config import discover_config_path, maybe_log_selected

Expand Down Expand Up @@ -67,6 +68,7 @@ def build_parser() -> argparse.ArgumentParser:
demo.register(subparsers)
export_logs_for_frontend.register(subparsers)
console.register(subparsers)
chat.register(subparsers)

reorder_subparsers_alphabetically(parser)
return parser
Expand Down
Loading