Findings 4-7 from the 2026-07-02 status analysis, per the session plan (S1): - CHANGELOG rewritten truthfully (was: 'Plan phase - no framework code yet') - README stack line names the split GA packages, not the agent-framework meta-package - CLAUDE.md: MCP downgraded to extension point (in-process FunctionTool is the default seam) - Verdict conflict semantics documented as chosen (store first-write-wins per id, disk/wiki last-write-wins per file; full B10 taxonomy deliberately deferred) - docs/extending.md: explicit 90%-principle cut-list (B10, B11, U12, U14, concurrent fan-out) - .gitignore covers .trekexecute-progress-* (docs/.DS_Store was already untracked/ignored - the plan's git rm --cached assumption was stale; no-op) No code behavior changed (docstring only in verdicts.py). Suite 152/4 green, mypy clean, ruff format --check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
122 lines
11 KiB
Markdown
122 lines
11 KiB
Markdown
# 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](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-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.
|
|
- `ruff` for lint+format. `pytest` for 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: in-process `FunctionTool` er default-sømmen i kjørestien; **MCP er extension point**, demonstrert via `build_mcp_server` (`datasource.py`) men ikke wiret inn i kjørestien. 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 delte
|
|
`shared/`-kjernen er ren stdlib — null `agent_framework`/`mcp`-import, så samme bundles
|
|
konsumeres uendret av begge stacker (D7-portabel). Håndhevet av
|
|
`tests/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 i
|
|
`test_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_project` overflater begge debatt-deltakere via `output_from=agents` og
|
|
overstyrer utfallet til en checker-kilde-`Rejection`). Gaten er opt-in-reject (fail-open ved
|
|
manglende markør), og `provenance.validator_decision` forblir ærlig — den speiler KUN validatoren,
|
|
aldri checkeren (de to falsifisererne blandes aldri). Load-bearing:
|
|
`tests/test_checker_gate_loadbearing.py` blir rød ved BEGGE detach-punkt (revert `output_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_llm`s ytre
|
|
`max_attempts`-løkke er ikke lenger blind — validatorens *forrige* `Rejection.reason` mates 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.py` blir 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øring `load_verdicts_from_dir`
|
|
→ `store.add` **merger** 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_project` persisterer 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, kontrast `okf.load_ir_projection`s fail-fast); `id` leses
|
|
verbatim, re-mintes aldri. `write_verdict` er den offentlige authoring-primitiven (persona/test +
|
|
framtidig Steg 8), men wires IKKE inn i `run_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).
|
|
- **Gated wiki-promotering (Steg 8, målbilde §3/§6/§7):** når en ekspert/persona GODKJENNER et
|
|
utfall, løfter `verdicts.promote_verdict` det fra RAW output-laget inn i kontekst-laget (OKF-bundelen)
|
|
som en `type: verdict`-konseptfil, navigerbar av neste kjørings `seed_store_from_bundle`. **Gaten er
|
|
fail-closed:** en ikke-godkjent dom (`decision ∉ {approved, approved_with_adjustment}`) raiser
|
|
`PromotionRefused` og skriver/linker INGENTING — kun menneske/persona-godkjent kunnskap når wikien,
|
|
aldri rå agent-output (selv-forurensning). Provenance-stemplet (hvem/eksperiment/når; `timestamp` er
|
|
påkrevd keyword, ingen wall-clock-default → deterministisk). **OKF-skriveren bor i `okf.py` og er ren
|
|
stdlib** (D7-portabel, MAF-fri — håndhevet av `test_okf_is_maf_free`); navigasjon følger KUN
|
|
index-cross-links, så `promote_verdict` linker filen i `index.md` via en NØYTRAL label (ellers lekker
|
|
signalet inn i `index_summary`→`bundle_context` utenom gaten). **R4 = valgfri+gated:** `promote_verdict`
|
|
er en offentlig opt-in-primitiv, wires IKKE inn i `run_project` (speiler `write_verdict` — systemet
|
|
leser; gaten/personaen promoterer). Ærlighets-grenser: promotert fil er MINIMAL (læringssignal kun som
|
|
`description`/body-prosa, reproduserer ikke seedens strukturerte `realization_rate` o.l.); id =
|
|
læringsnøkkel, så to godkjenninger om samme kandidat deler filnavn (last-write-wins, som `write_verdict`)
|
|
— wikien vokser én kuratert fil per distinkt kandidat, ikke per dom-hendelse. Load-bearing-trio
|
|
(`tests/test_step8_promotion_loadbearing.py`): gaten avviser ikke-godkjent dom (RØD uten gate); godkjent
|
|
dom er navigerbar (RØD når `link_in_index` detaches); promotert signal holdes ute av `bundle_context`
|
|
(RØD når en beskrivende index-label lekker det inn). Index-RMW er ikke-atomisk (enprosess-MVP).
|
|
- **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.
|
|
- **Offline simulering = primært metode-bevis (kostnadsdrevet, erstatter §11.8):** operatøren kjører
|
|
IKKE MAF mot ekte modell (verken Azure/Foundry eller Ollama — API for begge repoene er for kostbart
|
|
privat). `portfolio_optimiser.simulation` driver `run_project` med en SKRIPTET syntetisk chat-klient
|
|
(`ScriptedChatClient` på `OpenAIChatCompletionClient` — IKKE bare `BaseChatClient`, ellers no-op-er
|
|
`BudgetMiddleware`) over to kjøringer adskilt av en promotering, og viser at læringssløyfa lukkes:
|
|
Run A's godkjente persona-dom (markør fraværende fra bundelen) → `promote_verdict` → re-seed → Run B's
|
|
hypotese-prompt bærer markøren (tom-wiki-kontroll på Run A beviser kausalitet). **Ærlighet (§1,
|
|
ufravikelig):** beviser plumbing + deterministisk ryggrad + at dataflyten lukkes — IKKE at en levende
|
|
LLM ville produsert forslaget/dommen (skriptede stand-ins). Den genuine modell-atferd-sammenligningen
|
|
lever på Claude-SDK-siden (minimal API-kjøring). Skriptet klient = MAF-side stillas, IKKE delt
|
|
(`shared/` forblir framework-nøytralt). Kjøres `uv run python -m portfolio_optimiser.simulation`.
|
|
Load-bearing: `tests/test_simulation_loadbearing.py` blir RØD når promoteringen detaches.
|
|
- **Delt ekspert-persona som Agent Skill (§8, framework-nøytral):** ekspert-reviewer-personaen bor i
|
|
`shared/skills/expert-reviewer/` (`SKILL.md` + `references/example-verdict.json`) og er den ENE
|
|
delte artefakten begge stacker instansierer reviewer-en fra. `shared/` forblir REN DATA — MAF-siden
|
|
leser den via `portfolio_optimiser.persona.load_persona_example` (call-time, fail-fast), Claude-SDK-
|
|
søskenet med sin egen loader mot samme JSON. Dette AV-STUBBER simuleringen: persona-dommen (decision
|
|
+ rationale + sporet markør) hentes nå fra artefaktet ved call-time, ikke en inline-literal — så
|
|
personaen er genuint konsumert og kan ikke råtne stille. **Decision er binær** (`approved`/`rejected`
|
|
— `FeedbackContract` run-stien tar; `approved_with_adjustment` avvises der, bor kun i bundle-seedens
|
|
frontmatter + promoterings-gaten); realiseringskorreksjonen lever i rationale-prosaen, ikke et tredje
|
|
enum. SKILL.md-prosaen nevner ALDRI en konkret framework (maf-guarden er import-formet). Load-bearing-
|
|
trio (`tests/test_persona_skill_loadbearing.py`): struktur+framework-nøytralitet (RØD på framework-
|
|
import), eksempelet er gyldig pipeline-input inkl. `FeedbackContract` (RØD på skjema-/kontrakt-drift,
|
|
på en throwaway-kopi — aldri den git-tracked fixturen), og sim-ens markør følger artefakt-fila (RØD i
|
|
det øyeblikk personaen re-inlines).
|
|
- **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.)
|