The short loop captured the expert verdict inline into an in-memory store, so a verdict arriving days/weeks later in a separate run could not influence any future hypothesis (målbilde §5 row 7). Steg 7 adds the long timescale: run_project gains an opt-in verdict_dir async inbox that load_verdicts_from_dir -> store.add MERGES into the store BEFORE the Step-1 ExpeL fold, so a verdict dropped after an earlier run reaches a separate, later run's hypothesis — fully resumable across runs separated in time. - verdicts.py: verdict_to_dict / verdict_from_dict (id read verbatim, never re-minted), write_verdict (public authoring primitive, NOT wired into run_project — system reads the folder, expert/persona writes it, §3 role split), tolerant load_verdicts_from_dir (missing/foreign/half-written files skipped, not raised — RAW layer per §10 R2), VerdictStore.from_dir. - run.py: verdict_dir kwarg; ingest-merge block after load_contracts (merge not replace keeps run_portfolio's cross-project threading; store.add idempotent on content-hash id; no change to the fold). CLI --bundle-dir/--verdict-dir thread the long loop to the console entry. No auto-persist of the run's own captured verdict (outbox/Steg 8). - Load-bearing PAIR (test_step7_async_loop_loadbearing.py): a verdict dropped after run A must reach run B's prompt (run B uses a FRESH store -> the transfer is the file loop, not in-memory carryover); empty-inbox control proves causality. Marker = a realization value absent from the bundle (not the seed's 0.82). Proven RED on ingest detach. Suite 138 -> 140 passed, 4 skipped; mypy + ruff check clean. Målbilde treated as frozen (no §3/§5/§7 edit). Step 8 (gated wiki promotion) remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
6.7 KiB
portfolio-optimiser
Kontekst
Generisk, åpent Python-rammeverk på Microsoft Agent Framework (MAF) som finner kostnadsbesparelser INNI hvert prosjekt i en portefølje av uavhengige prosjekter. Multi-agent samarbeid genererer kandidat-tiltak; en obligatorisk deterministisk validator avgjør verdiene; fageksperter vurderer via HITL, og systemet lærer av dommene. Publiseres på Forgejo for alle som har MAF.
Bakgrunn og beslutninger: research (§15 implementeringsregister), plan. Løpende state: STATE.md (local-only).
Stack
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 (
mypyder mulig). Pydantic for validering/IR. rufffor lint+format.pytestfor test.- Modell-valg som konfig (modell-map rolle→Foundry-deployment), ikke spredt i kode.
- Metode kodifiseres som Agent Skill (
agentskills.io:SKILL.md+scripts/+references/). - Datatilgang via MCP-servere; data-source-konfig JSON-Schema-validert, fail-fast.
Kommandoer
- Sync:
uv sync - Test:
uv run pytest - Lint:
uv run ruff check .+uv run ruff format . - Type:
uv run mypy src
Arbeidsflyt (invarianter)
- Rent teknisk rammeverk: deployer eier DPIA/ROS/behandlingsformål. Bygg IKKE compliance-funksjoner — kun tekniske forutsetninger (lokal-only, provenance, ingen stille egress) + disclaimer.
- 90%-prinsipp: bygg den generiske kjernen + tydelige extension points; jakt IKKE de siste 10 %.
- Deterministisk validator er obligatorisk og blokkerende — aldri valgfri plugin.
- Framework-nøytral kontekst-søm: OKF-bundle-navigasjon (
okf.py) og den delteshared/-kjernen er ren stdlib — nullagent_framework/mcp-import, så samme bundles konsumeres uendret av begge stacker (D7-portabel). Håndhevet avtests/test_okf.py::test_okf_is_maf_free; importér aldri MAF inn i kontekst-laget. - OKF-navigert bundle-kontekst (ikke stuffing): på bundle-stien bygges agent-lese-konteksten
ved å NAVIGERE bundelen (
okf.bundle_context: index + frontmatter + cross-links, progressiv disclosure) — aldri keyword-chunk-stuffing (målbilde §2/§4).type: verdict-laget ekskluderes fra denne konteksten: tidligere dommer når hypotese-prompten KUN via den gatede ExpeL-folden. Load-bearing:test_bundle_context_excludes_verdict_layer+ empty-store-kontrollen itest_step1_expel_loadbearing.py(realiseringssignalet lekker aldri inn via kontekst). - Stoppkriterier + budsjett-tak påkrevd ved oppstart (fail-fast, aldri ubegrenset loop).
- Group Chat maker-checker som debatt-default (IKKE Magentic, som er eksperimentell).
- To falsifiserere, samme kandidat (Steg 3/4, målbilde §2/§6): den deterministiske validatoren
gater tallene (blokkerende), checkeren gater resonnementet. Checkeren avslutter turen med en
VERDICT: APPROVE/VERDICT: REJECT — <grunn>-linje; et eksplisitt avslag blokkerer et ellers validert forslag (run_projectoverflater begge debatt-deltakere viaoutput_from=agentsog overstyrer utfallet til en checker-kilde-Rejection). Gaten er opt-in-reject (fail-open ved manglende markør), ogprovenance.validator_decisionforblir ærlig — den speiler KUN validatoren, aldri checkeren (de to falsifisererne blandes aldri). Load-bearing:tests/test_checker_gate_loadbearing.pyblir rød ved BEGGE detach-punkt (revertoutput_from, eller fjern override). Checkeren «må faktisk gate, ELLER vi slutter å kalle det maker-checker». - Informert forbedring, bundet (Steg 5, målbilde §5/§7):
generate_via_llms ytremax_attempts-løkke er ikke lenger blind — validatorens forrigeRejection.reasonmates inn i neste forsøks prompt (_build_messages(prior_rejection=...)), så proposeren korrigerer i stedet for å gjenta. Kun den mest-nylige falsifiseringen (last, ikke akkumulert), kun grunnen (aldri forrige proposal-JSON), under EKSISTERENDE tak (meter.tick_round+max_attempts— ingen ny løkke; «forbedre til god nok» uten tak er forbudt). Eneste per-forsøk-falsifiserer her er validatoren; å seede generering med checker-kritikken er run-nivå og separat scoped (IKKE bygget her) — så koden påstår ikke mer enn den gjør. Load-bearing:tests/test_step5_refine_loadbearing.pyblir rød når reason-injeksjonen detaches (utfallet flipper aldri + reason-verbatim-asserten faller); kontrollen beviser at løkka forblir bundet. - Lang/async fil-løkke (Steg 7, målbilde §3/§7):
run_project(verdict_dir=...)er den lange tilbakemeldings-tidsskalaen — en ekspert/persona dropper en verdict-fil (vanlig JSON, RAW-laget per §10 R2) i en inbox-mappe ETTER en kjøring, og en separat, senere kjøringload_verdicts_from_dir→store.addmerger den inn FØR Steg-1-folden (ingen endring i folden), så dommen når neste hypotese. Rolledeling (§3, ufravikelig): systemet LESER mappa; eksperten/personaen SKRIVER den —run_projectpersisterer ALDRI sin egen fangede dom tilbake (det er outbox/Steg 8). Merge, aldri erstatt (run_portfolio-tråding intakt); tolerant last (manglende mappe / fremmede / halvskrevne filer hoppes over, ikke raises — RAW-lag, kontrastokf.load_ir_projections fail-fast);idleses verbatim, re-mintes aldri.write_verdicter den offentlige authoring-primitiven (persona/test + framtidig Steg 8), men wires IKKE inn irun_project. Load-bearing:tests/test_step7_async_loop_loadbearing.py— en dom droppet etter Run A MÅ nå Run B's prompt (Run B bruker FERSK store → overføringen er fil-løkka, ikke in-memory-carryover); tom-inbox-kontroll beviser kausalitet. Markør = realiseringsverdi som finnes ingen steder i bundelen (ikke frøets 0.82). - Kostnadsdisiplin: utvikle primært på lokal profil (gratis); Foundry/Azure (privat tenant finnes) kun til målrettet, minimal verifisering; billigste modeller + små syntetiske data + harde token-tak. Ingen tunge test-kjøringer.
- STATE.md er local-only (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
- Prosess: Voyage-plugin (
/trekbrief → /trekplan → /trekexecute → /trekreview) per større fase.
Communication patterns
When linking to local files in responses, use named markdown links — [Human-friendly name](file:///absolute/path), never bare file:// URLs or autolinks <file://...>, always absolute paths (never ~/ or relative), one bullet per file when there are several. (Bare file:// URLs render only the first as clickable across multiple lines; named links stay independently clickable.)