diff --git a/CLAUDE.md b/CLAUDE.md index d2e2dd7..40e9847 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Generisk, åpent Python-rammeverk på **Microsoft Agent Framework (MAF)** som fi Bakgrunn og beslutninger: [research](docs/research/2026-06-23-prior-art-platform.md) (§15 implementeringsregister), [plan](docs/plan/2026-06-23-incremental-plan.md). Løpende state: `STATE.md` (local-only). ## Stack -Python ≥3.10. MAF (`agent-framework` 1.8.0). Pakkehåndtering: `uv`. To backend-profiler: Azure/Foundry (full) + lokal (fallback). +Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To backend-profiler: Azure/Foundry (full) + lokal (fallback). ## Konvensjoner - Type hints overalt (`mypy` der mulig). Pydantic for validering/IR. diff --git a/docs/fase1-spikes/README.md b/docs/fase1-spikes/README.md new file mode 100644 index 0000000..ee8a3ab --- /dev/null +++ b/docs/fase1-spikes/README.md @@ -0,0 +1,76 @@ +# Fase 1 — De-risk spikes (A–D) + +> **Throwaway** spikes. Their only job is to turn the research §15 register's most +> dangerous *documented-but-unverified* assumptions into **measured facts** before +> the Fase 2 vertical slice. Code here is expected to be discarded once the +> findings are recorded — see [Disposal](#disposal). + +## Why these four + +Before committing to the full architecture in **Fase 2**, we empirically de-risk +the four assumptions that — if wrong — force a redesign: + +| Spike | Assumption (register ref) | What it measures | +|-------|---------------------------|------------------| +| **A** | Group Chat maker-checker beats a single-agent baseline by enough to justify its multiplicative token cost (U3 / G7) | convergence rounds, stall frequency, token use — maker-checker vs single-agent, with a cheaper/better verdict | +| **B** | The known MAF footguns behave as predicted and our guards hold: Magentic unbounded termination when `limits=None` (G1/B4); shared-builder / fan-out state corruption (G2/B7) | guard fires on unbounded Magentic; zero state-bleed with the fresh-instance helper | +| **C** | A blocking deterministic hybrid-validator (B1) can *structurally* block an out-of-range proposal | structural rejection of an out-of-range proposal; P10/P50/P90 for a valid one; capped self-repair | +| **D** | ExpeL retrieval (B2) surfaces a relevant prior verdict for a similar new proposal | top-K retrieval returns the structurally-similar verdict over surface-text decoys | + +Each spike produces a short findings note (`findings-{a,b,c,d}.md`) with a +**confirmed/refuted** verdict and a **token-use line** (including `0 — no live LLM` +where the spike runs without an endpoint). + +## Resolved version gate (premise-verified at planning time) + +- Installed `agent-framework-core` = **1.9.0** (introspection / `uv pip show`). +- The orchestration builders (`GroupChatBuilder`, `ConcurrentBuilder`, + `MagenticBuilder`, `StandardMagenticManager`, `TerminationCondition`) live in the + separate package **`agent-framework-orchestrations`**, GA **1.0.0** + (`requires agent-framework-core<2,>=1.9.0` — exactly our installed core). +- Because the spikes are **throwaway**, this package + `pulp` (Spike C solver) + + `pytest-asyncio` (MAF orchestrations are async) are pinned in the **`dev`** + optional-dependency group — **not** core. Promotion to core is a Fase 2 decision. + +## Gate-green contract + +Each spike splits into: +- a **logic layer** — pure functions/classes we author, always exercised by the + quality gate (no live endpoint, no full MAF workflow run); and +- an **integration/live layer** — drives the real MAF builders and/or a real LOCAL + LLM; runs when available, otherwise `skip`s (except Spike B, where driving the + builders with a fake client IS the de-risk). + +The headline empirical claims (Spike A's better/cheaper verdict; live token numbers) +live in the integration/live layer and are honestly reported as **endpoint-dependent**. +The gate stays green from the logic layer alone. + +## Status + +| Spike | Assumption | Result | Confirmed/Refuted | Implication for Fase 2 | +|-------|-----------|--------|-------------------|------------------------| +| A | maker-checker > single-agent (U3/G7) | _pending_ | — | — | +| B | Magentic unbounded + fan-out bleed (G1/G2) | _pending_ | — | — | +| C | blocking hybrid-validator (B1) | _pending_ | — | — | +| D | ExpeL retrieval (B2) | _pending_ | — | — | + +_(Consolidated in Step 7 once each spike's findings note is written.)_ + +## Disposal + +These spikes are throwaway. To remove them completely after the findings are recorded: + +```bash +rm -rf spikes tests/spikes docs/fase1-spikes +``` + +Then revert the three `pyproject.toml` edits that supported them: + +1. Remove the dev deps `agent-framework-orchestrations`, `pulp`, `pytest-asyncio` + from `[project.optional-dependencies].dev`. +2. Restore `[tool.ruff] src = ["src", "tests"]` (drop `"spikes"`). +3. Restore `[tool.pytest.ini_options] pythonpath = ["src"]` and remove + `asyncio_mode = "auto"`. + +Then `uv sync --extra dev` to regenerate `uv.lock`. Nothing in `src/` is touched by +the spikes, so disposal leaves the framework core exactly as Fase 0 left it. diff --git a/pyproject.toml b/pyproject.toml index bc6fac6..538deff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,10 @@ dev = [ "pytest>=8", "ruff>=0.6", "mypy>=1.11", + # Fase 1 throwaway de-risk spikes (dev-only — NEVER shipped in the wheel; safe to delete). + "agent-framework-orchestrations>=1.0.0", # GA orchestration builders (GroupChat/Concurrent/Magentic); resolves with core 1.9.0 + "pulp>=2.8", # Spike C solver — PuLP bundles a CBC binary in its wheel (R2) + "pytest-asyncio>=0.24", # MAF orchestrations are async (await workflow.run(...)) ] [build-system] @@ -31,8 +35,9 @@ packages = ["src/portfolio_optimiser"] [tool.ruff] line-length = 100 -src = ["src", "tests"] +src = ["src", "tests", "spikes"] [tool.pytest.ini_options] -pythonpath = ["src"] +pythonpath = ["src", "."] testpaths = ["tests"] +asyncio_mode = "auto" diff --git a/spikes/__init__.py b/spikes/__init__.py new file mode 100644 index 0000000..a9a3b57 --- /dev/null +++ b/spikes/__init__.py @@ -0,0 +1,11 @@ +"""Throwaway Fase 1 de-risk spikes — dev-only, never shipped in the wheel; +safe to delete after findings are recorded. + +These modules empirically turn the research §15 register's most dangerous +*documented-but-unverified* assumptions into measured facts before the Fase 2 +vertical slice (see ``docs/fase1-spikes/README.md``). They are intentionally +isolated at the repo top level so the hatchling wheel (which packages only +``src/portfolio_optimiser``) never ships them, and disposal is a single +``rm -rf spikes tests/spikes docs/fase1-spikes`` plus reverting the ``dev``/ +``tool`` ``pyproject.toml`` edits. +""" diff --git a/tests/spikes/__init__.py b/tests/spikes/__init__.py new file mode 100644 index 0000000..272b18c --- /dev/null +++ b/tests/spikes/__init__.py @@ -0,0 +1,3 @@ +"""Throwaway Fase 1 de-risk spike tests — dev-only, never shipped in the wheel; +safe to delete after findings are recorded. +""" diff --git a/tests/spikes/test_imports.py b/tests/spikes/test_imports.py new file mode 100644 index 0000000..9ee1950 --- /dev/null +++ b/tests/spikes/test_imports.py @@ -0,0 +1,29 @@ +"""Step 1 import smoke: the throwaway spikes package resolves, and the dev-only +orchestration + solver + async dependencies are importable. + +Pattern: tests/test_smoke.py (plain functions, no fixtures). +""" + + +def test_spikes_package_imports() -> None: + import spikes # noqa: F401 (repo-root on pythonpath; no collision with installed dists) + + +def test_orchestration_builders_import() -> None: + # The GA orchestration builders live in the separate `agent-framework-orchestrations` + # package (NOT top-level agent_framework, NOT core) — confirmed by introspection + PyPI. + from agent_framework.orchestrations import ( # noqa: F401 + ConcurrentBuilder, + GroupChatBuilder, + MagenticBuilder, + StandardMagenticManager, + TerminationCondition, + ) + + +def test_solver_imports() -> None: + import pulp # noqa: F401 (Spike C — bundles a CBC binary in its wheel) + + +def test_async_runner_imports() -> None: + import pytest_asyncio # noqa: F401 (MAF orchestrations are async) diff --git a/uv.lock b/uv.lock index bd3ed0f..9713fc1 100644 --- a/uv.lock +++ b/uv.lock @@ -50,6 +50,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/bf/4185b88aad70f61947e19d4c94e85561282fd2310ea3ffb3cf4b4de22327/agent_framework_openai-1.8.2-py3-none-any.whl", hash = "sha256:02bb6b323d58dfeab48de4816ae1b0083202edaa93e4f4874d7054d39d9067a4", size = 58378, upload-time = "2026-06-18T09:42:56.459Z" }, ] +[[package]] +name = "agent-framework-orchestrations" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/98/1f943e3a383bf37af24f1cc3ab3812fbe2041b65bc2e2f82626352907190/agent_framework_orchestrations-1.0.0.tar.gz", hash = "sha256:7f4d26de1ac0b2add3742964836927df44138e79d16bbf084d6e866f1d4b5aff", size = 60805, upload-time = "2026-06-18T09:43:02.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/cc/64448d22e95db33bb7701d3a2bb8888d39f2fa8f4e410bd24232417c9ea3/agent_framework_orchestrations-1.0.0-py3-none-any.whl", hash = "sha256:1f0885c3de69c919bb631f7e4e24727d94733b135a1529ab88ffa47bf18a2878", size = 66828, upload-time = "2026-06-18T09:42:59.363Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.2" @@ -365,6 +377,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/0b/e106f0fd7fa785867d9ffcc47dc9e6237c0e58f51058473b777487a98edc/azure_storage_blob-12.30.0-py3-none-any.whl", hash = "sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423", size = 435610, upload-time = "2026-06-08T11:45:37.213Z" }, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + [[package]] name = "certifi" version = "2026.6.17" @@ -1324,8 +1345,11 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "agent-framework-orchestrations" }, { name = "mypy" }, + { name = "pulp" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "ruff" }, ] @@ -1334,9 +1358,12 @@ requires-dist = [ { name = "agent-framework-core", specifier = ">=1.9.0" }, { name = "agent-framework-foundry", specifier = ">=1.8.2" }, { name = "agent-framework-openai", specifier = ">=1.8.2" }, + { name = "agent-framework-orchestrations", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11" }, + { name = "pulp", marker = "extra == 'dev'", specifier = ">=2.8" }, { name = "pydantic", specifier = ">=2.11,<3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, ] provides-extras = ["dev"] @@ -1469,6 +1496,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "pulp" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/70/69be07a67621ad804d6cf347965eb4e0d7786a97330d99c31d735aaa6c5a/pulp-3.3.2.tar.gz", hash = "sha256:d0904700c207ac11e25e3b1213b70eae1d6fb25faa719d75f3f15054901258c0", size = 16305346, upload-time = "2026-05-25T09:41:26.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/6e/d674f1dde91c71e2ac19e5e5cf1ee6d5845e3aefecd9c39ed9c4b0c9a696/pulp-3.3.2-py3-none-any.whl", hash = "sha256:631b166f72086971a9597f7a0233ababa99bb8d50a01cd543f7758be5a9f86c0", size = 16391742, upload-time = "2026-05-25T09:41:22.2Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1653,6 +1689,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2"