portfolio-optimiser/docs/plan/2026-08-23-magentic-utforskningssloeyfe.md
Kjell Tore Guttormsen 4a19d39e63 feat(tracing): U14 - sporing er opt-in, og "av" betyr at MAF aldri kalles (ORDRE 20260823T165757Z)
PORTFOLIO_OTEL er eneste bryter, lest paa truthiness. Uten den kalles
configure_otel_providers ikke i det hele tatt: spans lages fortsatt
(ENABLE_INSTRUMENTATION defaulter True) og kastes, saa ingenting KAN forlate
prosessen. `console` skriver spans til stderr - demoens stdout er byte-identisk
med fasiten, maalt. `otlp` eksporterer over nett, og kun mot et endepunkt
operatoeren selv har navngitt.

To regler er MAALT, ikke valgt (observability.py:849 bygger exporter-lista i
fast rekkefoelge):

- enable_console_exporters sendes EKSPLISITT False i begge moduser. Overlatt til
  miljoeet faller den tilbake paa ENABLE_CONSOLE_EXPORTERS, hvis
  ConsoleSpanExporter skriver til STDOUT - nettopp det S6 maalte som oedeleggende
  for goldenen.
- `console` NEKTER naar en OTEL_EXPORTER_OTLP_*_ENDPOINT finnes: env-avledede
  exportere bygges UBETINGET og FOER vaare, saa ordet "console" ville vaert en
  usann paastand om hvor kjoeringens innhold tok veien. Validering, ALDRI
  reparasjon - vi fjerner ikke operatoerens variabel bak ryggen paa dem.

Tre kallsteder (run.main, simulation.main, hosting.main): demoen er et skriptet
bevis, ikke produktet, og en soem bare demoen naar ville latt de to inngangene en
virksomhet faktisk kjoerer vaere usporbare. tracing_notice er ENESTE renderer og
returnerer None naar sporing er av - omisjon, aldri tom rad.

IKKE bygget, med grunn: PLAN_CREATED/REPLANNED/PROGRESS_LEDGER_UPDATED hoerer til
sloeyfa U4 bygger; en emitter uten kallsted er en form gjettet i stedet for maalt.
OTLP-exporter-PAKKENE er bevisst ikke deklarert (egress + grpc/protobuf-vekt i et
publisert wheel); uttalt i README/DEPLOY/env.template.

Ny dep: opentelemetry-sdk>=1.42,<2 (operatoerbeslutning 2, 23.08). EN pakke, ikke
to - ConsoleSpanExporter bor inne i sdk-en. opentelemetry-api fulgte med
1.42.1 -> 1.44.0, maalt uskadelig.

Load-bearing MAALT (tests/test_tracing_loadbearing.py), ni mutasjoner alle roede
mot HELE suiten + groenn kontroll 943/5. Tre av de roede bor i tester som fantes
fra foer (golden-transkriptets fire-linjers stderr + portefoelje-CLI-ens stille
pass), altsaa er omisjons-regelen gatet av uavhengige vitner.

Golden ea8c534... uendret. mypy src + ruff rene.

Ordren tar ogsaa de fire operatoerbeslutningene inn i planens paragraf F.
Laasen paa orchestrations 1.0.1 er ENDELIG (operatoerbekreftelse 23.08), ikke
midlertidig: spike-ordrens "revert hvis E7 staar" er overstyrt av den senere
beslutningen, som betinget paa groenn suite - ikke paa E7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:51:46 +02:00

652 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MAF-utnyttelsesrevisjon U1U15 + plan for Magentic-basert utforskningssløyfe
> **Ordre** `20260823T125528Z-1876024635-from-.claude` · **Økt 53**, 2026-08-23 · Fable 5/xhigh uten
> advisor (operatør-overstyring). Planarbeid: ingen kode i `src/`, ingen tester. **Hvert tall i dette
> dokumentet kommer fra en kommando som ble kjørt denne økten** (vedlegg V1V3); alt som ikke ble
> målt er merket «ikke målt» eller «spike».
## 0. Sammendrag — de seks setningene som bærer resten
1. **Målingen:** av `.claude`s ni «null i src/»-påstander holder 7 ordrett; U5 og U11 trenger
presisering (artefaktet/sitatbæringen finnes, MAF-konstruktet gjør det ikke). Nevner: 32 filer.
2. **Magentic-status:** hele Learn-siden hentet — **ingen experimental-advarsel for Python**
(eneste «experimental» er et C#-pragma), og installert kode har ingen `@experimental`-markør
på Magentic. F14-sitatet «untested … outside of the original Magentic-One design» **står
fortsatt**. PyPI latest er 1.1.1 (Production/Stable), men bare 1.0.0/1.0.1 kjører på core 1.9.0.
3. **Designet:** Magentic legges **OVER** den normative 8-stegs-sløyfa, ikke inni Steg 3. Utforskningen
er en **mandat-former**: prompt + bundles (+ fagpersonens frø-hypoteser) → `Mandate`
`run_project(mandate=…)` **uendret**. Validatoren gater hvert tall i nøyaktig samme gate som i dag.
4. **Fire ting ble MÅLT på installert 1.0.0, ikke antatt:** en Magentic-workflow er single-use
(høylytt `RuntimeError`), men **builder og manager-instans blør** (4/4 kontaminering, worker
hoppet over, fabrikkert sluttsvar); plan-review-rundturen **virker**, men har **ingen
revise-cap**; `max_round_count` gir en **kanonisk streng, aldri et typet signal**;
`MagenticResetSignal` **nullstiller ikke** deltakernes sesjon (orphan-attributt i 1.0.0).
5. **U14 er ikke «én funksjon»:** `setup_observability` finnes ikke; inngangen er
`agent_framework.observability.configure_otel_providers`, og **`opentelemetry-sdk` er ikke
installert** — kallet krasjer med `ModuleNotFoundError`. U14 er en avhengighetsbeslutning.
6. **Rekkefølgen:** S0S6-spikes (1 økt) → U14 (1) → U4+U13 synkron (2) → U12 + asynkron U13 (1)
→ U15 som egen sammenligning (1). U1/U2/U5/U11 avvises med begrunnelse. Seks økter.
---
## A. U1U15-måling (nevner: 32 `.py`-filer i `src/`; kontroll: 117 i `tests/`, 6 i `spikes/`)
Metode: `grep -rEn --include='*.py' "<regex>" src` per kapabilitet, samme regex mot `tests/` og
`spikes/` som kjent-positiv-kontroll, og — for hver regex som ga 0 i alle tre — en tredje kjøring
mot installert MAF (`.venv/lib/python3.12/site-packages/agent_framework{,_orchestrations}`) som
beviser at spørringen KAN finne (Verifiseringsloven ansikt 4). Skript og rå output: vedlegg V1.
En uavhengig andre sveip (Opus-agent, 35 tokens, samme nevner) ga identiske nuller (V3).
| ID | Kapabilitet (§15.1) | src | tests | spikes | venv-kontroll | Status | Belegg / spørring |
|----|----|---:|---:|---:|---:|----|----|
| U1 | `ConcurrentBuilder` fan-out | 0 | 4 | 4 | — (tests/spikes finner) | **IKKE** — erstattet av håndrullet bølge | `run.py:1262` `await asyncio.gather(` (D-D). Regex `ConcurrentBuilder\|BuildConcurrent\|FanIn` |
| U2 | `WorkflowBuilder` graf | 0 | 0 | 0 | 21 filer | **IKKE** | Regex `WorkflowBuilder\|@executor\|class .*\(Executor\)\|WorkflowContext\|SwitchCase\|add_edge\|add_fan_out` |
| U3 | `GroupChatBuilder` debatt | 10 | 7 | 7 | — | **LANDET** | `workflow.py:24` import · `:104` `GroupChatBuilder(` · `:108` `termination_condition=` · `:113` `.with_max_rounds(max_rounds)` |
| U4 | Magentic | 0 | 4 | 9 | 4 filer | **IKKE** | tests-treff: `tests/spikes/test_b_footguns.py`, `tests/spikes/test_imports.py`, `tests/test_method_spec_loadbearing.py:40` (framework-navn-svarteliste for spec-prosa — ikke bruk). `.claude`s «kun spikes/b_footguns.py» er riktig for BRUK |
| U5 | Agent Skills (`SkillsProvider`) | 0 | 0 | 0 | 3 filer | **IKKE** som MAF-konstrukt; **DELVIS** som artefakt | `persona.py:3`/`:47` leser `shared/skills/expert-reviewer/SKILL.md` via EGEN `load_persona_example`, aldri `SkillsProvider`. Installert core: `@experimental(feature_id=ExperimentalFeature.SKILLS)``SkillsProvider` (`_skills.py:1718`) |
| U6 | MCP-tools | 6 | 6 | 0 | — | **LANDET** (opt-in) | `mcp_tools.py:42-43` import · `:154` `MCPStdioTool(` · `:170` `MCPStreamableHTTPTool(`. Ingest-sømmen bruker rå `mcp.ClientSession` (`ingest_mcp.py:144-179`) |
| U7 | Function tools | 4 | 10 | 0 | — | **LANDET** | `datasource.py:18` `from agent_framework import FunctionTool, tool` · `:65` · `:69` `@tool(` |
| U8 | Middleware | 16 | 20 | 0 | — | **LANDET** | `budget.py:27` · `:228` `class BudgetMiddleware(ChatMiddleware)` · `run.py:635/:645` |
| U9 | `ContextProvider` | 8 | 11 | 4 | — | **LANDET** | `verdicts.py:29` · `:319` `class ExpeLContextProvider(ContextProvider)` · `run.py:724/:843` |
| U10 | Vektorlager | 2 | 19 | 0 | — | **DELVIS** (egen, ikke MAF) | `semretrieval.py:355` `save_vector_store` · `:393` `load_vector_store` — numpy brute-force (D-C). 0 på `AzureAISearch\|cosmos\|qdrant\|redis\|VectorStore` |
| U11 | Citation-aware RAG | 26 | 35 | 0 | — | **DELVIS** (egen, med MAF-typer) | `provenance.py:18` `from agent_framework import Annotation, TextSpanRegion` · `:78` `to_annotations()` («display only, NOT the source of truth»); `datasource.py:1` «citation-bearing data source». 0 på `AIContextProvider\|TextSearchProvider\|agentic_retrieval\|KnowledgeAgent` |
| U12 | Checkpointing | 3 (kun prosa) | 0 | 0 | 14 filer | **FRAVALGT** — §A.1 | `run.py:22`, `workflow.py:14`, `:115`: «no checkpoint — research 01: durable resume is fragile». 0 kode-treff på `checkpoint_storage\|CheckpointStorage` |
| U13 | HITL-gates | 3 | 0 | 0 | — | **DELVIS, og UØVD** | `workflow.py:116` `builder.with_request_info(agents=[agents[-1]])` bak `enable_layer1_hitl=False` (`:78`, `run.py:524`; ingen CLI-flagg). ENESTE test: `tests/test_workflow.py:54` `test_layer1_hitl_option_builds` — bygger, kjører aldri gaten, sender aldri et svar. `run.py` awaiter `debate.run` til ferdig (`:684-686`) og leser aldri strømmen. 0 på `RequestInfoEvent\|approval_mode\|MagenticPlanReview` i src OG tests |
| U14 | OpenTelemetry | 0 | 0 | 1 | 4 filer | **IKKE** | Regex `opentelemetry\|setup_observability\|OBSERVABILITY\|get_tracer\|start_as_current_span`. `uv pip list`: KUN `opentelemetry-api 1.42.1` (transitiv) — ingen sdk, ingen exporter |
| U15 | Evaluering | 0 | 0 | 0 | 2 filer | **IKKE** | Regex `evaluate_workflow\|LocalEvaluator\|Evaluator\|_evaluation`. `evaluate_workflow` finnes (`_evaluation.py:1833`) under `@experimental(feature_id=ExperimentalFeature.EVALS)` (`:1832`); 20 `@experimental` i modulen |
Utenfor de 15 (samme kjøring): U16 `CompactionProvider` 0/0/0 (venv 4) · U17 `as_tool(` 0/0/0
(venv 1) · U18 `FoundryChatClient` **LANDET** `backends.py:150` · U19 Harness 0/0/0 (venv 1).
**Mot `.claude`s ni null-påstander:** 7 av 9 holder ordrett (U1, U2, U4, U12, U13, U14, U15). 2 av 9
trenger presisering: **U5** og **U11**. De 6 landet-påstandene holder (U3, U6, U7, U8, U9,
U10-delvis). Aliasing/re-eksport er utelukket: alle 14 `agent_framework`-importlinjer i `src/` er
listet i V3 og hver er et direkte navn; `from agent_framework_orchestrations import` → 0 treff
(importstien i bruk er `agent_framework.orchestrations`, `workflow.py:24`).
### A.1 U12 — fravalgt på et premiss som har råtnet, eller som holder?
Premisset i koden: «research 01: durable checkpoint resume is fragile» (`workflow.py:14`).
**«research 01» finnes ikke som dokument i repoet** (`grep -rn -i "research.01" docs/ STATE*.md`
→ 0). Nærmeste dokumenterte grunnlag: kapabilitetskartet
(`docs/research/2026-06-24-maf-capability-map.md:54`): «superstep-granular (not mid-LLM-call);
resume needs identical graph. Defer past MVP unless needed»; egnethetsplanen 08-09 P9/P10
(«pass-nivå-gjenopptakelse er levert via spend-fila; ingen checkpointing»); `docs/extending.md:315`
(«A run either completes or is re-run; the async verdict inbox is the resumable boundary»).
**Vurdering:** begrunnelsen holder **for dagens debatt** (sekunderminutter; re-kjøring er billigere
enn gjenopptakelse). Den holder **ikke** for målbildet: en utforskning som stopper i
`MagenticPlanReviewRequest` og venter på en fagperson i dager kan ikke holde en prosess i live, og
«re-kjør» kaster planen fagpersonen skulle revidere. **Premisset er ikke råttent — det er utenfor
sitt gyldighetsområde.** Det som ble MÅLT i installert core 1.9.0 denne økten (V2 §7): mekanismen
FINNES og dekker Magentic-tilstand — `FileCheckpointStorage` (`_checkpoint.py:239`), «restore then
send» (`run(responses=…, checkpoint_id=…)` eksplisitt tillatt, `_workflow.py:877`), pending
`request_info` re-emitteres ved resume (`_runner_context.py:414-426`), og `MagenticOrchestrator.
on_checkpoint_save` skriver kontekst (runde/stall/reset-tellere), ledgere og managerens hele sesjon
(`_magentic.py:1266-1284`, `:746`). «Skjør» er i dag konkret: base64-pickle av tilstand med
allow-list (`_checkpoint.py:242-250`), graf-signatur må matche (`_runner.py:275-279`), og
manager-state-feil ved lagring/gjenoppretting **svelges med warning** (`_magentic.py:1281-1282`,
`:1320-1321`). Ingen ende-til-ende-resume med pending plan review er kjørt → **spike S4** (§D).
---
## B. Magentic-status mot primærkilden (hele siden hentet 2026-08-23)
Kilde: `microsoft_docs_fetch` mot
`learn.microsoft.com/en-us/agent-framework/workflows/orchestrations/magentic` — hele siden.
1. **Ingen experimental-advarsel for Python.** Eneste «experimental» på siden: C#-blokken
`#pragma warning disable MAAIW001 // Magentic types are experimental` (.NET-diagnostikk).
`.claude`s fravær er **bekreftet**, ikke bare ikke-motbevist.
2. **F14-sitatet står ordrett:** «it is untested how well the Magentic orchestration will perform
outside of the original Magentic-One design», og tipset «consider using the Group Chat pattern
instead» for enklere koordinering. F14s VURDERING var riktig lest; operatøren har flyttet
PREMISSET (målbildet krever planlegging), ikke sitatet. G8 i §15.3 er halvt riktig: funksjonelle
workflows er fortsatt `ExperimentalFeature.FUNCTIONAL_WORKFLOWS`; Magentic er ikke markert.
3. **Installert kode:** `agent_framework/_feature_stage.py:43-65``ExperimentalFeature` har 13
medlemmer (DECLARATIVE_AGENTS, EVALS, FILE_HISTORY, FIDES, FOUNDRY_TOOLS, FOUNDRY_PREVIEW_TOOLS,
FUNCTIONAL_WORKFLOWS, HARNESS, MCP_LONG_RUNNING_TASKS, MCP_SKILLS, PROGRESSIVE_TOOLS, SKILLS,
TO_PROMPT_AGENT). **Magentic er ikke der**; `grep -n -i "experimental\|feature_stage"
agent_framework_orchestrations/*.py` → 0.
4. **PyPI** (`curl https://pypi.org/pypi/agent-framework-orchestrations/json`): latest **1.1.1**
(2026-08-21), `Development Status :: 5 - Production/Stable`, 26 utgivelser. Krav per versjon:
1.0.0/1.0.1 → core ≥1.9.0; 1.0.2/1.1.0 → ≥1.13.0; 1.1.1 → ≥1.15.0. Core latest 1.15.0 (21.08).
5. **Installert = pinnet:** `uv pip list` → orchestrations 1.0.0, core 1.9.0; `uv.lock:61-63` låser
1.0.0; `pyproject.toml:17` `>=1.0.0`, `:12` core `>=1.9.0,<2` (`tests/test_maf_version_guard.py`).
6. **Learn-siden beskriver LATEST API** (`manager_agent=`, `enable_plan_review=`,
`intermediate_output_from=`). Installert 1.0.0 har de samme konstruktør-navnene (V2 §2) — men
builder-docstringen i 1.0.0 beskriver et HITL-API (`MagenticHumanInterventionRequest`,
`with_human_input_on_stall`) som **ikke finnes i koden** (kun i docstrings, `_magentic.py:1388-1559`).
Designet i §C er skrevet mot det som er målt installert, aldri mot docstring eller Learn.
7. **Core 1.12.0-notatet (21.07, samme minutt som orchestrations 1.0.1):** «Prevent the Magentic
manager from duplicating conversation history (#6297)». Om 1.0.1 bærer fiksen er **ikke
verifisert** — det er spike S0.
---
## C. Design: den utforskende sløyfa
### C.0 Beslutningen — Magentic OVER sløyfa, ikke INNI Steg 3
Kjernespørsmålet: Magentic gir manageren frihet til å velge vei; validatoren gater utfallet. Er de
forenlige? **Ja — på én betingelse: utforskningen produserer KANDIDATER, aldri FORSLAG.**
Tre fakta avgjør formen:
1. **Metodespesifikasjonen er normativ og commons-eid** (`shared/method-spec.md`, pull-only). §3
Steg 3 KREVER en to-rolle maker-checker-debatt, rundetak og fersk tilstand per kjøring; Steg 2
krever «exactly one candidate measure» per proposer-kall; §8 «Never an unbounded loop, anywhere».
En Magentic-sløyfe som ERSTATTER Steg 3 bryter spec-en og krever commons-amendment FØR bygging —
og den ville flyttet validatoren INN i grafen. Sømkartet (V3) er entydig: alt som gjør et svar
etterprøvbart ligger UTENFOR workflow-grafen og ETTER `debate.run` — generering `run.py:748-759`,
validator `generate.py:491`, provenance `run.py:793`, checker-override `:816`, dimensjonsgate
`:826`, domfangst `:849`, outbox `:857`.
2. **`Mandate` finnes og ER «en prompt om hva som skal gjøres»** (`mandate.py:55-88`): `objective` +
`approaches[{id, label, description}]` + `allow_own_proposals` + `success_criteria`. En
`Approach.description` er «the expert's own prose reason … fed to the proposer VERBATIM»
(`mandate.py:44-49`, `generate.py:308-317`) — altså «hypoteser fagpersoner kommer med».
`_evaluate_mandate` (`run.py:292-344`) kjører hver approach gjennom `generate_via_llm`
`validate_proposal` under SAMME meter, og rapporterer `not_evaluated` for det budsjettet ikke
rakk (aldri utelatt). `announce` (`mandate.py:130-183`) sier høyt hva kjøringen skal gjøre FØR
første betalte kall — repoets eksisterende før-kjøring-menneskesjekk.
3. **Validatoren er ren og offline** (`validate_proposal`, `validator.py:194-260`; CBC via PuLP,
`_MC_SAMPLES = 512`, ingen `agent_framework`-import) og kan eksponeres som `FunctionTool` (U7)
uten å endre hva den er.
Derfor: **utforskningen er en MANDAT-FORMER.** Inn: operatørens prompt + tilgjengelige bundles
(+ valgfrie frø-approaches fra fagpersoner). Ut: en `Mandate` hvis `approaches` er hypotesene
utforskningen fant verdt å teste, med managerens begrunnelse som `description`, og `bundle_id` per
approach. Deretter `run_project(bundle_dir=…, mandate=…)` **uendret**: debatt (Steg 3) → generering
(Steg 2) → deterministisk validator (Steg 4, blokkerende, stempler `validator_decision`) →
checker-gate → outbox/HITL (Steg 68). Ingen linje i den kjeden endres. Manageren velger VEI i et
rom som ikke kan skrive til outbox eller wiki; validatoren gater hvert TALL som forlater rommet, i
samme gate som i dag.
**Tre nivåer av garanti (uttalt, så ingen leser mer inn i det enn det er):**
| Nivå | Hva | Hvor | Stempel? |
|---|---|---|---|
| 1 | `quick_validate`-verktøyet i utforskningen | samme `validate_proposal`, samme baseline, kalt av hypothesiser-agenten | **NEI** — rådgivende tall i en utforskningslogg, aldri provenance |
| 2 | Pipelinen per approach | `run.py:748` `_evaluate``validate_proposal` | **JA**`ProvenanceStamp.validator_decision` |
| 3 | Skriverettigheter | kun pipelinen når outbox (`outbox.write_outbox`) og wiki (`promote_verdict`, gated) | utforskningens sluttsvar er RÅ (`{run_id}-exploration.json`), aldri et forslag |
Ærlighetsregelen (§1) følger direkte: managerens «final answer» presenteres ALDRI som et resultat.
Resultatet er det `_evaluate_mandate` returnerer.
### C.1 Manager + deltakersett (mot installert 1.0.0-API, V2 §23)
| Rolle | Type | Gjør | Verktøy / kanal |
|---|---|---|---|
| **manager** | `MagenticBuilder(manager_agent_factory=…)``StandardMagenticManager(agent=…)` med EGNE ledger-prompter (konstruktør-parametre `task_ledger_*_prompt`, `progress_ledger_prompt`, `final_answer_prompt`) | planlegger, velger neste taler, fører progress ledger, replanlegger | ingen — men **`BudgetMiddleware` PÅ manager-agenten** (C.2) |
| **navigator** | `Agent` | leser bundles PROGRESSIVT (§3 Steg 1) og svarer med sitater | `list_bundles()` · `read_bundle(id)``okf.navigate_bundle` + `Bundle.index_summary()`/`bundle_context` · `read_file(id, path)` innenfor `retrieval.safe_resolve` |
| **hypothesiser** | `Agent` | former én kandidat om gangen som IR-JSON (gjenbruker `generate._build_messages` + `proposal_response_format()`) | `quick_validate(ir_json)``{decision, reason, p10, p50, p90}` (nivå 1) |
| **expert_liaison** | menneske/persona | svarer på spørsmål, reviderer retning | plan review (`MagenticPlanReviewRequest.approve()/.revise(feedback)`) — og, etter spike S3b, `AgentApprovalExecutor` som deltaker for svar MIDT i kjøringen |
**Hvorfor `StandardMagenticManager`, ikke egen subklasse:** egen manager = egen ledger-parsing = egen
kopi av Magentic-One-logikken (kø-(p): den andre kopien drifter). Prompt-overstyring dekker behovet
(norsk domene, IR-disiplin, «aldri oppgi et tall uten `quick_validate`»). **Målt grunn til å
overvåke:** manageren parser ledgeren med en håndrullet JSON-skraper (`_extract_json`,
`_magentic.py:406-449`, `ast.literal_eval`-fallback), 3 forsøk (`:588-590`, `:710-723`) → `RuntimeError`;
`MagenticProgressLedgerItem.answer` er ikke typesjekket per felt (`:300-302`). Egen manager FØRST når en
målt mangel krever det.
**Hvorfor `manager_agent_factory=` og ikke `manager_agent=`:** målt E4 (V2 §6) — `manager_agent=`
konstruerer manageren eagerly (`:1683`) og gir SAMME instans til hvert `build()` (`:1729-1730`);
factory-varianten kalles per build (`:1732-1738`).
### C.2 Progress ledger som styringssignal, koblet mot `budget.py`
Ledgeren (`_magentic.py:310-314`, navn verifisert): `is_request_satisfied` / `is_in_loop` /
`is_progress_being_made` / `next_speaker` / `instruction_or_question`. Stall-regelen (`:1109-1118`):
`not is_progress_being_made or is_in_loop``stall_count += 1`, ellers **dekrementer** (ikke
nullstill); `stall_count > max_stall_count` (strengt `>`) → `_reset_and_replan` (`:1149`): kontekst
nullstilles, `reset_count += 1`, replan, plan review på nytt med `is_stalled=True` hvis aktivert.
To signaler, to vakter, aldri slått sammen (S3.4-invarianten):
| Signal | Kilde | Vakt | Utfall |
|---|---|---|---|
| **Semantisk** — stall | ledger | `max_stall_count` → reset+replan → `max_reset_count` | `exploration_stop="stalled"` — strukturert; IKKE krasj, IKKE suksess |
| **Ressurs** — tokens | `BudgetMiddleware` (`budget.py:228`) på HVER agent **inkl. manager** | pre-call-guard `:242-244` | `BudgetExceeded(kind="tokens")` → 429-kanalen |
| **Ressurs** — runder | Magentic `max_round_count` (= `Budget.max_rounds`) | intern (`:1238`) | **oversettes** av vårt lag til `BudgetExceeded(kind="exploration_rounds", limit, observed)` |
**Målt grunn til oversettelsen (E5, V2 §4):** `max_round_count`/`max_reset_count` gir **ingen
exception** — en kanonisk assistent-melding «Workflow terminated due to reaching maximum round
count.» + `_terminated=True` (`:1222-1256`), uten `prepare_final_answer`. Ved transporten er det
uskillbart fra suksess. Vårt lag leser derfor `round_count` fra `MagenticContext` (eksponert via
`PROGRESS_LEDGER_UPDATED`-eventene, ett per runde) og `orchestrator._terminated`, og produserer det
typede stoppet SELV — slik `BudgetExceeded`/429 alt gjør for debatten. Strengen pinnes mot den
installerte konstanten (`ROUND_CAP_LOGGER`-presedensen), aldri fri-tekst-matchet.
**`tick_round` er IKKE koblet til orkestreringsrunder i dag** (sømkart: to kallere, begge i
`generate.py:390/:455`; `run.py:632` gir meteret `max(max_rounds*4, 4)` for GENERERINGS-forsøk).
Utforskningen får derfor sin egen `Budget` (tokens + runder) i en egen `TokenMeter`, og `run_project`
sitt meter er urørt. Stall-telleren er IKKE en budsjettvakt og kobles ikke inn i `TokenMeter`.
Hver runde logges (ledger + `next_speaker` + `instruction_or_question`) til
`{run_id}-exploration.json`, så en stoppet utforskning er lesbar uansett hvilken vakt som fyrte.
**⚠ Spike S2:** manageren er et `Agent` (`StandardMagenticManager(agent=…)`, `:528`; ett
`create_session()` i `__init__`, `:572`; alle kall via `agent.run(…, session=…)`, `:601`), så
agent-nivå `ChatMiddleware` SKAL fyre — men det er **ikke målt** for manager-stien, og en feil her
gjør «harde token-tak» til en løgn for den mest pratsomme deltakeren. Kontroll: bar
`BaseChatClient` no-op-er middleware (`simulation.py:373-375`) — spiken bruker `ScriptedChatClient`.
### C.3 Grensene — B4 bygges ikke bort
| Dagens | Magentic (installert) | Regel |
|---|---|---|
| `with_max_rounds(max_rounds)` (`workflow.py:113`) | `max_round_count` (ctor-kwarg; default `None` = ubegrenset, `:542`) | PÅKREVD, positiv, fail-fast (G1/B4/A4) |
| — | `max_stall_count` (default 3, `:540`) | PÅKREVD, positiv — ingen arv av defaulten |
| — | `max_reset_count` (default `None`, `:541`) | PÅKREVD, ≥0 |
| termination-safety-net (`:108`) | manageren avslutter ved `is_request_satisfied` (`:1104-1107`) **eller** ved ukjent `next_speaker` (`:1128-1131` — stille sluttsvar!) | rundetaket ER nettet; ukjent taler logges som avvik |
| — | plan review: **ingen revise-cap, revises telles ikke som runder** (målt E6: 2 manager-kall per revise, 0 ledger-kall) | NY `max_plan_revisions` i kontrakten; hver revise `tick_round()`-es |
`ExplorationContract` (pydantic, fail-fast, speiler `contracts.TerminationContract`): `max_rounds`,
`max_tokens`, `max_stall_count`, `max_reset_count`, `max_plan_revisions`, `enable_plan_review`.
Ingen defaults: en utforskning uten uttalte grenser nekter å starte.
### C.4 B7 — tilstandsisolasjon. MÅLT (V2 §6; skript i V1)
| Eksperiment | Resultat | Konsekvens |
|---|---|---|
| **E1** én bygget workflow, to `.run()` | run 2 → `RuntimeError: This Magentic workflow has already been completed…` (`_magentic.py:914-918`), **0 modellkall** | single-use — STERKERE enn GroupChat 1.9.0s stille tomme kjøringer [2,0,0]; kan ikke korrumpere, for den kan ikke kjøre |
| **E2** to workflows, ÉN delt `StandardMagenticManager` | run 2s manager-kall ser run 1s oppgave **4/4**; meldingstall vokser 2→…→23→**26**→39 over kjøringsgrensen; run 2s første ledger svarte `satisfied=True` og **workeren ble aldri kalt (0)** — et fabrikkert sluttsvar | ikke bare kost: **en plausibel fasit produsert av null arbeid** |
| **E3** kontroll, fersk manager per build | **0/5** kontaminering, 5 kall inkl. `ledger_UNSAT` (workeren KALT) | 5-vs-4-kallformen er diskriminatoren |
| **E4** én `MagenticBuilder`, to `.build()` | samme 4/4-blødning som E2 (`:1683`, `:1729-1730`) | «ny workflow» er ikke nok — **ny BUILDER per oppgave**, eller `manager_agent_factory=` |
| **E7** `MagenticResetSignal` | `_magentic.py:1366` skriver `self._agent_thread` — lest av INGENTING (1 treff repo-vidt; den levende er `AgentExecutor._session`, 7 treff). Målt: cache/full_conversation tømt, **sesjonsidentitet uendret** | stall-replan gir fersk manager-ledger men **gamle deltakere**; docstringens løfte (`:1341-1344`) holder ikke i 1.0.0 |
**Regel:** per utforskning **ny builder → ny build → én kjøring**, fersk klient per agent (som
`fresh_workflow`/`_default_factory` alt gjør, `run.py:436-440`). Stol aldri på stall-reset for å
tømme deltakerminne. `fresh_exploration_workflow()` speiler `fresh_workflow()` (`workflow.py:74`).
### C.5 HITL-spranget: fra JA/NEI til «be om svar, bruke svarene»
**Installert flyt (målt E6, V2 §5):** `with_plan_review()`/`enable_plan_review=True`
`ctx.request_info(MagenticPlanReviewRequest{plan, current_progress, is_stalled},
MagenticPlanReviewResponse)` (`:1041-1048`) → event `type="request_info"` med `request_id`
kjøringen STOPPER uten output → `workflow.run(responses={request_id: req.approve()})` eller
`req.revise("…")` (`_workflow.py:899-908`) → ved revise: feedback inn i historikken, `manager.replan`,
`REPLANNED`-event, **ny review-forespørsel** (`:1014-1031`) → ved approve: løkka kjører.
Dette er målbildets «be om svar, bruke svarene og jobbe videre» ordrett — og det virker på 1.0.0.
**Ingen landingsplass i dag** (sømkart §4): `with_request_info` bygges men ingenting konsumerer
strømmen; `hitl.py` er et offline inspeksjonsverktøy (id-join outbox inbox, `hitl.py:159`), ikke
en forespørsel/svar-mekanisme. Koblingen mot den asynkrone køa **gjenbruker formen, ikke filene:**
| Eksisterende | Utforskningen |
|---|---|
| outbox `{run_id}-outcome.json` med `verdict_id` | **NY artefaktklasse** `{run_id}-plan-review.json` = `{request_id, plan, current_progress, is_stalled, checkpoint_id}` — samme RAW-lag, samme mappe, byte-deterministisk via `outbox._dump` |
| inbox `{id}.json` (`verdicts.write_verdict`) | `{request_id}.json` = `{decision: approve\|revise, feedback}` — eksperten SKRIVER, systemet LESER (§3 Steg 7-rolledelingen) |
| `hitl.py pending` (id-join) | samme join over den nye klassen; `python -m portfolio_optimiser.hitl pending` viser begge |
| `FeedbackContract.decision ∈ {approved, rejected}` | **IKKE gjenbrukt** — en plan-revisjon er ikke en dom; eget lite pydantic-skjema |
| `simulation`-personaen (`load_persona_example`) | spiller eksperten SYNKRONT i demoen: `revise("Test også …")` én gang, så `approve()` |
To tidsskalaer, som Steg 7: **synkron** (persona/operatør i samme prosess — demoen og første
leveranse) og **asynkron** (fil-innboks over dager → krever U12: `checkpoint_id` i artefaktet,
`run(responses=…, checkpoint_id=…, checkpoint_storage=…)`). Det er DÉT som flytter U12 fra «NEI» til
«nå» — og spike S4 avgjør om 1.9.0 bærer det.
**Svar MIDT i kjøringen (ikke bare ved plan):** plan review er det ENESTE `request_info`
orkestratoren selv reiser (4 treff i `_magentic.py`, 0 i basen). Men `AgentApprovalExecutor`
(`_orchestration_request_info.py:168`, `propagate_request=True` `:201`) kan stå som DELTAKER:
når manageren velger `expert_liaison`, blir dens output en `request_info`, og mennesket svarer med
`AgentRequestInfoResponse.approve()`/`.from_strings(…)` (`:55-79`) — svaret blir deltakerens
output i historikken. **Ikke målt** → spike S3b. Lander den, er «stille spørsmål … be om svar»
dekket både ved plan og underveis.
### C.6 «Hypoteser fagpersoner kommer med» — tre inngangsdører, i kostnadsrekkefølge
1. **I mandatet (finnes):** `Approach{label, description}` → proposeren verbatim. Utforskningen tar
frø-approaches som INPUT og **MÅ bevare dem i output-mandatet** (en hypotese fagpersonen ba om
forsvinner aldri stille — `not_evaluated`-regelen anvendt på utforskning).
2. **I plan review (U13):** `revise("Test hypotesen at …")` → replan med teksten i task-ledgeren.
3. **Underveis (S3b):** `expert_liaison`-deltakeren — manageren SPØR, fagpersonen svarer, svaret
blir kontekst for neste runde.
4. **I kunnskapsbasen (senere, commons-sak):** `type: hypothesis`-konseptfil navigatoren finner.
Krever OKF-vokabular-beslutning hos commons — **ikke i denne planen**.
### C.7 OKF-bundles som inngang — hva manageren må vite
Manageren planlegger mot en **katalog**, aldri innholdet (§3 Steg 1). Per bundle: `id` ·
`Bundle.index_summary()` (`okf.py:154`) · kandidat-features (`verdicts.bundle_candidate_features`,
`:482`) · **om `cost-baseline.json` finnes** (`okf.load_optional_cost_baseline`, `:411`) — avgjørende:
en uforankret bundle kan ikke quick-valideres mot stage 0, og manageren skal vite hvilke hypoteser
som KAN testes deterministisk · antall `type: verdict`-filer (`Bundle.verdicts`). Navigatoren åpner
én bundle om gangen på managerens instruks. **Multi-bundle er NYTT:** `run_project` tar ÉN
`bundle_dir` (`run.py:514`; `bundle_dirs` → 0 treff); utforskningen tar `bundle_dirs`, hver
approach bærer `bundle_id`, og pipelinen kjøres per bundle som i dag (`run_portfolio`-formen).
### C.8 Modulplassering (for byggeøktene, ikke for denne)
`src/portfolio_optimiser/explore.py` (NY): `ExplorationContract`, `ExplorationResult{mandate,
ledger_log, stop, plan_reviews}`, `fresh_exploration_workflow(...)`, `explore(...)`. Importerer
`agent_framework.orchestrations`**aldri** fra `okf.py`/`mandate.py`/`hitl.py` (MAF-fri-guarden
`test_okf_is_maf_free`). Verktøyene (`quick_validate`, navigator-tools) bor i `explore.py` og
wrapper de rene modulene. `run.py` får ett nytt kallsted (`--explore "<prompt>"``explore()`
`run_project(mandate=…)`), `hosting.py` ett nytt whitelistet felt (`explore_prompt`), og
`simulation.py` ett nytt scenario. Demo-transkriptet (`tests/golden/demo-transcript.stdout`) er
**byte-uendret** fordi utforskningen er opt-in.
---
## D. Prioritert vei gjennom de ni gapene
### D.0 Versjonsbeslutningen (FØR noe bygges — én avgjørelse, ikke en glidning)
**Anbefaling: bygg mot 1.0.x (installert) i denne planen.** (1) core-bumpen 1.9→1.15 er seks
minor-versjoner over privat-API-premisser `test_maf_version_guard` vokter, og tre målte
1.9-egenskaper (`ExperimentalWarning`-paret, single-use GroupChat [2,0,0], middleware-no-op på bar
klient) må re-måles; (2) en bump er en egen beslutning, ikke et biprodukt av sløyfa; (3) **1.0.1 er et
patch-hopp på samme core** som kan tas gratis
hvis `uv lock --upgrade-package agent-framework-orchestrations` gir 1.0.1 og suiten er grønn — og
som KAN bære #6297 (§B pkt. 7) og/eller E7-fiksen. Det er spike S0. Bumpen til 1.1.x/core 1.15 er
en EGEN økt etter denne planen (bonus: `InvocationsHostServer` krever core ≥1.13) — ikke en forutsetning.
### D.1 Spikes (økt 54 — alle i `spikes/e_magentic.py` + `tests/spikes/`, aldri `src/`)
| # | Spørsmål | Måling | Felles hvis |
|---|---|---|---|
| S0 | Gir 1.0.1 samme API og fjerner den E2/E4/E7? | `uv lock --upgrade-package agent-framework-orchestrations``uv pip list`; re-kjør V1-skriptene `magentic_b7*.py` + `magentic_reset.py`; `uv run pytest -q` | E7 står → bli på 1.0.0, noter |
| S1 | B7 i repoets egen form | E1E4 + E7 portert til `spikes/` med `ScriptedChatClient` (ikke agentens `FakeClient`) | 4/4 på delt builder, 0/5 på fersk |
| S2 | Fyrer `BudgetMiddleware` på MANAGERENS kall? | manager-`Agent(middleware=[BudgetMiddleware(meter)])`, `Budget(max_tokens=1, …)`; assert `BudgetExceeded` propagerer ut av `workflow.run` OG at `meter.tokens > 0` etter ett kall | middleware fyrer ikke → egen klient-wrapper; plan-endring |
| S3 | Plan-review-rundtur i repoets form | E6 portert: request_info-event → `run(responses=…)` med `revise``approve`; tell manager-kall per revise (målt 2) | rundturen feiler på 1.0.0 |
| S3b | `AgentApprovalExecutor` som deltaker | svar injiseres via `from_strings("svar")`, assert svaret står i neste manager-prompt | ikke nåbar → kun plan review (dør 1+2) |
| S4 | Resume med pending plan review | `with_checkpointing(FileCheckpointStorage(tmp))`, kjør til review, **ny prosess** (subprosess), `run(responses=…, checkpoint_id=…, checkpoint_storage=…)`; assert `request_info` re-emittert + sluttsvar | feiler → U12 synkron-først, asynkron etter planen |
| S5 | `quick_validate`-latens | `validate_proposal` ×20 på `bygg-energi-baseline-mikro` med `time.perf_counter` | > 2 s/kall → budsjettér kall i kontrakten |
| S6 | OTEL gratis? | `uv add opentelemetry-sdk` i scratch-venv; `configure_otel_providers(enable_console_exporters=True)`; kjør demoen; tell spans | ingen `workflow.run`-span → U14 er større enn én økt |
### D.2 Rekkefølgen
| # | Hva | Gap | Load-bearing for målbildet? | Økt | Modell |
|---|---|---|---|---|---|
| 0 | **Spikes S0S6** | U4/U13/U12/U8/U14 | JA — hver er en antakelse planen ellers bygger blindt på | 54 | Opus 5/xhigh |
| 1 | **U14 OpenTelemetry**`opentelemetry-sdk` som deklarert dep, `configure_otel_providers` bak `PORTFOLIO_OTEL=…` (console default; OTLP KUN med eksplisitt `OTEL_EXPORTER_OTLP_ENDPOINT` — «ingen stille egress»), spans på DAGENS pipeline først; egne events for `PLAN_CREATED`/`REPLANNED`/`PROGRESS_LEDGER_UPDATED` (målt: `_magentic.py` emitterer null spans selv) | U14 | JA for «stole på» — og baselinen U15 måler mot | 55 | Opus 5/high |
| 2 | **U4 + U13 synkron**`explore.py` (C.8), `ExplorationContract`, mandat-former, `quick_validate`, navigator-tools, plan review med persona; `simulation`-scenario; `--explore` i CLI | U4, U13 | JA — dette ER målbildet | 5657 | Opus 5/xhigh |
| 3 | **U12 + asynkron U13**`FileCheckpointStorage`, `{run_id}-plan-review.json` + innboks-svar, `hitl.py pending` utvidet, `--resume <run_id>` | U12, U13 | JA for svar over dager — **betinget av S4** | 58 | Opus 5/xhigh |
| 4 | **U15** — EGEN sammenligning på golden-bundelen: utforskning vs. debatt på tre tall (validerte kandidater, tokens, runder), skrevet til `docs/` som måling | U15 | JA for «finner den bedre tiltak» — men MAF-evals AVVISES (eksperimentell i kode, «wrong shape», kapabilitetskart :58) | 59 | Opus 5/high |
| — | U1 `ConcurrentBuilder` | U1 | NEI — bølgemodellen er målt, bærer budsjett-reservasjon (S3.4); Concurrent har ingen runder å reservere mot | etter planen, om noensinne | — |
| — | U2 `WorkflowBuilder` | U2 | NEI — Magentic ER graf-laget; direkte adopsjon gir null ny kapabilitet | nei | — |
| — | U5 `SkillsProvider` | U5 | NEI — `ExperimentalFeature.SKILLS`; egen loader virker; commons eier innholdet | etter planen | — |
| — | U11 MAF RAG-providere | U11 | NEI — «navigate, never stuff» forbyr query-time retrieval; sitatbæring finnes | avvist med begrunnelse | — |
Seks økter (5459); faller S4, sparer pkt. 3 én økt.
### D.3 For og mot de fire kandidatene ordren navngir
**U4 + U13 tidlig — FOR:** selve målbildet; alt annet er instrumentering av status quo. **MOT:** hviler
på umålte antakelser (S2, S3b, S4) og på en `request_info`-sti repoet ALDRI har kjørt. Derfor spikes
først — ikke omvendt. **Plass #2 og #3.**
**U14 tidlig — FOR:** (1) repoets deploy-gate; (2) uten sporing er en utforskning en svart boks — og
Magentic-koden gir selv null spans, så ledger-eventene må vi logge; (3) LITE i kode: én funksjon +
env; (4) instrumentert FØR utforskningen gir U15 sin baseline. **MOT (målt):** det er **ikke gratis i
avhengigheter** — `opentelemetry-sdk` + exporter må deklareres (wheel-vekt, `uv sync`, handover-
pakka), og `ENABLE_INSTRUMENTATION` er default `True` (`observability.py:697`) så spans LAGES alt i
dag og kastes. OTLP-exporter er egress og må være opt-in bak eksplisitt endepunkt. **Plass #1,
betinget av S6.**
**U15 — FOR:** uten måling kan vi ikke påstå at sløyfa er bedre; ærlighetsregelen forbyr påstanden.
**MOT `evaluate_workflow`:** eksperimentell i kode (20 dekoratorer) og feil form (kvalitetsscore
offline, ikke domeneobjekt). **Konklusjon:** evaluering JA, som egen sammenligning; MAF-flaten
NEI inntil `EVALS` forlater `ExperimentalFeature`. **Plass #4** — den trenger noe å måle.
**U12 — FOR:** asynkron plan review over dager er umulig uten; mekanismen dekker Magentic-tilstand
(målt i kode, §A.1). **MOT:** ende-til-ende-resume på 1.9.0 er umålt, restore-feil svelges, og en
synkron persona-variant leverer målbildet i demoen uten den. **Konklusjon:** S4 avgjør; bygg i #3 hvis
grønn, ellers synkron-først og U12 etter planen, sammen med core-bumpen.
### D.4 Det som IKKE gjøres (uttalt)
- Ingen endring i `shared/method-spec.md`. Utforskningen er et lag OVER §3; en amendment
(«Step 0 — Explore, optional») foreslås til commons via coord (operatørbeslutning, §F) — planen
avhenger ikke av svaret.
- Ingen D7-speiling nå: po-claude er PARKERT (STATE); utforskningslaget meldes dit når parkeringen
oppheves, på linje med Steg-5-returtypen og demo-innboksen som også venter.
- Ingen erstatning av GroupChat-debatten (Steg 3 står). Ingen `open/`-push, versjonsbump, Azure.
- Ingen egen `MagenticManagerBase`-subklasse før en målt mangel krever det.
---
## E. Verifisering — testbare kriterier (kommandoer, ikke «sjekk at det virker»)
**Denne økten (planen selv):**
| # | Kriterium | Kommando → forventet |
|---|---|---|
| E1 | Nevneren er 32 | `find src -name '*.py' \| wc -l``32` |
| E2 | Hver null-spørring kan finne | `bash scratchpad/u_measure.sh` (V1) → `venv_files` ≥ 1 for hver av de 9 null-regexene |
| E3 | Magentic ikke kode-markert | `grep -c -i "experimental" .venv/lib/python3.12/site-packages/agent_framework_orchestrations/_magentic.py``0`; `grep -n "SKILLS\|EVALS" …/agent_framework/_feature_stage.py``:54`, `:64` |
| E4 | Installert = lås | `uv pip list \| grep orchestrations``1.0.0`; `sed -n '61,63p' uv.lock``version = "1.0.0"` |
| E5 | Single-use + builder-blødning reproduserbar | `uv run python scratchpad/magentic_b7.py``E1 run2 … RuntimeError`, `E2 VERDICT … 4/4`, `E3 VERDICT … 0/5` |
| E6 | Plan-review-rundtur reproduserbar | `uv run python scratchpad/magentic_b7b.py``E6 after APPROVE outputs: ['FINAL: …']`, `E5 outputs: ['Workflow terminated due to reaching maximum round count.']` |
| E7 | OTEL-sdk mangler | `.venv/bin/python -c "import opentelemetry.sdk"``ModuleNotFoundError` |
| E8 | Planen er dok-gate-klassifisert | `uv run pytest -q tests/test_doc_constant_sync_loadbearing.py` → grønn (datert sti = arkiv-klasse) |
| E9 | Treet urørt utenom planen | `git status --porcelain` → kun `docs/plan/2026-08-23-…md` + den fremmede `presentasjon`-HTML-en |
**Byggeøktene (kriterier som FELLES når sømmen detaches — load-bearing-metoden):**
| Leveranse | Rødt-først-kriterium |
|---|---|
| S1/B7 | test som deler builder mellom to utforskninger er RØD (4/4); `fresh_exploration_workflow` gjør den grønn (0/N) |
| S2/budsjett | `Budget(max_tokens=1)` på manager-agenten → `BudgetExceeded` ut av `explore()`; detach middleware på manageren → testen rød |
| C.2/runder | `max_rounds=1``BudgetExceeded(kind="exploration_rounds", limit=1, observed=1)`; detach oversettelsen → kjøringen «lykkes» med den kanoniske strengen → rød |
| C.3/kontrakt | `ExplorationContract(max_stall_count=None)``ValidationError` ved konstruksjon |
| C.5/plan review | persona-`revise` én gang → `REPLANNED` i loggen og `plan_reviews == 2`; `max_plan_revisions=1` + persona som alltid reviderer → typet stopp, aldri evig løkke |
| C.6/frø | frø-approach `id="fagperson-1"` finnes i output-`Mandate.approaches` uansett hva manageren fant; detach bevaringen → rød |
| C.0/nivå 3 | `explore()` skriver ingenting til outbox/wiki: `ls outbox` før/etter → kun `{run_id}-exploration.json`; `git status shared/` ren |
| U12/S4 | subprosess-resume: pending `request_info` re-emittert + sluttsvar; detach `checkpoint_storage` → rød |
| U14 | demoen under `PORTFOLIO_OTEL=console` → ≥1 `workflow.run`-span på stderr; uten variabelen → stdout byte-identisk med `tests/golden/demo-transcript.stdout` |
| Golden | `uv run pytest -q` grønn; `shasum tests/golden/demo-transcript.stdout` uendret |
---
## F. Nøkkelantakelser, risiko og åpne beslutninger
**Alle åtte radene er MÅLT i økt 54** (ordre `20260823T162224Z`, spikes S0S6). Måleapparatet er
`spikes/e_magentic.py` + `tests/spikes/test_e_magentic.py` (16 tester), kjørt mot orchestrations
**1.0.1** på core 1.9.0; hele suiten 920 passed / 5 skipped, golden-transkriptet uendret
(`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`).
| # | Antakelse | Test | Status |
|---|---|---|---|
| A1 | Agent-nivå `ChatMiddleware` fyrer på managerens kall | S2 | **MÅLT GRØNN.** `BudgetMiddleware` på manager-agenten krediterer meteret (`meter.tokens == 8` etter ett kall) og stopper kjøringen. Detach-kontroll: uten middleware fullfører SAMME 1-token-budsjett. Planens budsjettgaranti står. |
| A2 | `BudgetExceeded` reist inni en Magentic-deltaker propagerer ut av `workflow.run` | S2 | **MÅLT GRØNN, og som repoets EGEN type** — ikke pakket i en `ExceptionGroup`: `isinstance(exc, BudgetExceeded)`, `kind="tokens"`, `limit=1`, `observed=8`. Trippelen kø-(y) leser er intakt, så 429-kanalen kan brukes uendret. |
| A3 | `request_info`-rundtur | S3 | **MÅLT GRØNN på 1.0.1.** Review stopper kjøringen uten output (manageren har da kun kalt `facts`+`plan`); `revise` koster nøyaktig 2 manager-kall (`facts_update`, `plan_update`), **null ledger-kall og null runder**, og **spør på nytt**; `approve` kjører løkka til sluttsvar. Bekrefter at `max_plan_revisions` MÅ inn i kontrakten — ellers er en alltid-reviderende ekspert et ubundet forbruk. |
| A4 | Resume med pending plan review i NY prosess | S4 | **MÅLT GRØNN.** Foreldreprosessen stopper på review og etterlater checkpoints; en subprosess (`spikes/e_magentic_resume.py`) som aldri så kjøringen svarer fra checkpointen alene og driver workflowen til sluttsvar. **Pris, ikke forutsett:** `FileCheckpointStorage` NEKTER å deserialisere `MagenticPlanReviewRequest`/`…Response` uten at begge navngis i `allowed_checkpoint_types` — uten det er checkpoint-fila uleselig og listingen TOM, altså en resume som feiler som et FRAVÆR. Begge prosesser må deklarere dem. |
| A5 | `AgentApprovalExecutor` som deltaker gir svar midt i kjøringen | S3b | **MÅLT GRØNN — dør 3 i C.6 står.** Ekspertens ord når både liaisonen og en senere manager-prompt. **To rundturer per menneskesvar:** `from_strings([svar])` mater svaret tilbake INN i liaisonen og gjenopptar IKKE manageren (målt: null manager-kall mellom de to forespørslene); først `approve()` sender liaisonens output videre. **Kostnad:** `AgentApprovalExecutor` er IKKE re-eksportert fra `agent_framework.orchestrations` (svartypen ER det) — døra koster i dag en privat-API-import. |
| A6 | Persona-manuset kan drive en Magentic-manager (ledger-JSON) offline | S1/S3 | **MÅLT GRØNN med repoets `ScriptedChatClient`** (ikke lenger en ad-hoc `FakeClient`). Ett forbehold funnet: selectoren får den SAMMENSLÅTTE prompten, så ett av fem manager-kall bærer to markører — rutingen må teste senere-stadium-markøren først. Og en `next_speaker` som ikke matcher en deltaker gir **stille sluttsvar uten at noen ble spurt** (`_magentic.py:1128-1131`), målt da liaison-spiken først het `worker`. |
| A7 | Utforskning som opt-in holder demo-transkriptet byte-uendret | `shasum` | **MÅLT uendret** gjennom hele økten (`ea8c534…`). |
| A8 | 1.0.1 er API-identisk med 1.0.0 | S0 | **MÅLT: API-identisk, ATFERD ikke.** Diffen er upstream-regresjonsfiksen #4371: `StandardMagenticManager.__init__` mistet `self._session = agent.create_session()`, og hvert manager-kall mynter nå en engangs-sesjon. **Konsekvens: E2 OG E4 er BORTE** (4/4 → **0/5** begge). E1 (single-use `RuntimeError`, null kall) og E7 (`MagenticResetSignal` skriver til orphan-attributtet `_agent_thread`, `:1369`, sesjonsidentitet uendret) står. Manager-sesjonen fjernet også fra checkpoint-state, konsistent med at manageren nå er tilstandsløs per kall. |
**Målt korreksjon til E-tabellen (U12/S4):** kriteriet «detach `checkpoint_storage` → rød» er
FEIL — å fjerne `checkpoint_storage=` fra `run()` lar HELE suiten stå grønn (920 passed), fordi
`.with_checkpointing(...)` på builderen allerede ga workflowen lageret. De to bærende punktene er
`checkpoint_id=` (fjernes → rød) og builderens `.with_checkpointing(...)` (fjernes → rød).
**Load-bearing MÅLT** (mot HELE suiten, grønn kontroll 920/5): `manager_keeps_persistent_session`
konstant `True` (2 røde — E2+E4 alene) · aldri fest `BudgetMiddleware` på manageren (3 røde, mens
detach-armen forblir grønn) · detach markør-registreringen (1 rød — S3b-positiven alene, kontrollen
grønn) · flipp `_route`-rekkefølgen så `pre-survey` testes først (4 røde) · resume uten
`checkpoint_id=` (1 rød) · builder uten `.with_checkpointing()` (1 rød) · tom
`_ALLOWED_CHECKPOINT_TYPES` (1 rød) · og den falsifiserte: resume uten `checkpoint_storage=` (**0
røde** — funnet over).
**S5 — `quick_validate`-latens: median 13,6 ms** over 20 kall på `bygg-energi-baseline-mikro`
(forankret baseline + assumption-bånd, så stage 0 + CBC + 512-sample Monte Carlo er alle med).
Langt under 2 s-terskelen: verktøyet kan kalles fritt i løkka, og kontrakten trenger ingen
egen latens-post. Båndet er med med vilje — uten det faller `_monte_carlo` tilbake på
`item.unit_cost`, alle draw blir identiske, og tallet ville underrapportert den ekte kostnaden.
**S6 — OTEL er gratis, men IKKE via `enable_console_exporters`.** I et scratch-venv pinnet til
samme stack (core 1.9.0 / orch 1.0.1 / `opentelemetry-sdk` 1.44.0; **ingenting lagt til
`pyproject.toml`**) gir `configure_otel_providers(enable_console_exporters=True)` **2
`workflow.run`-spans** — pluss `workflow.build`, `executor.process`, `edge_group.process`,
`message.send`, `invoke_agent`, `chat synthetic` — men de skrives til **stdout**, som ville
ødelagt golden-transkriptet. Med `exporters=[ConsoleSpanExporter(out=sys.stderr)]` kommer de 2
`workflow.run`-spanene på **stderr** og demoens stdout er **byte-identisk med fasiten** (samme
shasum). U14 er altså én økts arbeid — forutsatt at exporteren konstrueres eksplisitt mot stderr,
aldri via flagget.
**U14 er LANDET (økt 55, ordre `20260823T165757Z-6762213199`).** Operatøren tok alle fire
beslutningene som anbefalt 23.08, og bekreftet samme dag at **låsen på orchestrations 1.0.1 er
ENDELIG, ikke midlertidig** — spike-ordrens «revert hvis E7 står» er overstyrt av den senere
beslutningen, som betinget på grønn suite (920/5, målt) og ikke på E7. Levert:
* `opentelemetry-sdk>=1.42,<2` som DEKLARERT dependency (beslutning 2). Én ny pakke, ikke to:
`ConsoleSpanExporter` bor inne i sdk-en. `opentelemetry-api` fulgte med fra 1.42.1 til 1.44.0 —
målt uskadelig (golden `ea8c534…` uendret før og etter).
* `src/portfolio_optimiser/tracing.py``PORTFOLIO_OTEL` på truthiness, modusene `console`
(spans → stderr) og `otlp` (kun mot et navngitt endepunkt), `tracing_notice` som eneste renderer.
* Wiret i TRE inngangspunkter: `run.main`, `simulation.main`, `hosting.main`.
* **S6-forbeholdet er implementert som skrevet:** `ConsoleSpanExporter(out=…)`, aldri
`enable_console_exporters=True`. Målt: demoen under `PORTFOLIO_OTEL=console` gir `workflow.run`-
spans på stderr og stdout BYTE-IDENTISK med fasiten; uten variabelen null spans.
* **To ting E-tabellen ikke forutså, begge lukket:** (i) MAF bygger env-avledede OTLP-exportere
UBETINGET, så `console` NEKTER når en `OTEL_EXPORTER_OTLP_*_ENDPOINT` er satt — ellers var
«console» en usann påstand om hvor kjøringens innhold tok veien; (ii) `enable_console_exporters`
faller tilbake på `ENABLE_CONSOLE_EXPORTERS` i miljøet, så den pinnes eksplisitt `False` i begge
moduser. Begge er MÅLT som defektklasser, ikke antatt.
* **IKKE bygget, med grunn:** `PLAN_CREATED`/`REPLANNED`/`PROGRESS_LEDGER_UPDATED` — de hører til
sløyfa U4 bygger, og en emitter uten kallsted er en form gjettet i stedet for målt. De landes i
økt 5657, på sømmen U14 nettopp la. OTLP-exporter-PAKKENE er bevisst ikke deklarert (egress +
grpc/protobuf-vekt i et publisert wheel); uttalt ærlighets-grense i README/DEPLOY/env.template.
Suite etter U14: **943 passed / 5 skipped**, golden `ea8c534…` uendret, `mypy src` + `ruff` rene.
**Beslutninger som trenger operatøren (speiles i avslutningsblokken):**
1. **Versjon — AVGJORT 23.08: låsen står på 1.0.1, ENDELIG.** Bakgrunnen: ordrens «felles hvis» sa
at hvis E7 står i 1.0.1, revert til 1.0.0. E7 STÅR — men 1.0.1 fikset noe større enn det som ble
håpet (E2/E4, «en plausibel fasit produsert av null arbeid»). Operatøren aksepterte 23.08 at
gevinsten veier tyngre enn ordrens bokstav, og at betingelsen som gjelder er «grønn suite»
(målt 920/5), ikke E7. **Ikke midlertidig, og ikke å gjenåpne uten en ny måling.** Bumpen til
1.1.x / core 1.15 er fortsatt en EGEN økt etter denne planen.
2. **U14 som deklarert avhengighet — AVGJORT 23.08: ja.** Landet i økt 55; se «U14 er LANDET»
over. Kostnaden ble ÉN pakke (`opentelemetry-sdk`), ikke to.
3. **HITL — AVGJORT 23.08: synkron-først.** Merk at ordrens BEGRUNNELSE er utdatert mens
konklusjonen står: den sier «A4 er umålt og kun kode-lest», men A4 ble MÅLT GRØNN i økt 54, så
asynkron er teknisk mulig. Valget er dermed ren rekkefølge, ikke risiko — asynkron U13 forblir
pkt. 3 i rekkefølgen, betinget av S4 som nå er grønn.
4. **Commons-amendment «Step 0 — Explore» — AVGJORT 23.08: sendt.** `.claude` sendte forslaget til
`portfolio-optimiser-commons` via coord. Vi venter IKKE: planen slår selv fast at ingen amendment
er påkrevd, og «Step 0» er foreslått som VALGFRI. Kommer den, er den en tilføyelse.
---
## Vedlegg V1 — målingsskript og rå output (U1U19)
Skript (`scratchpad/u_measure.sh`, kjørt 2026-08-23; kopiert hit ordrett så målingen kan gjentas):
```bash
#!/bin/bash
cd "$(git rev-parse --show-toplevel)"
SRC_N=$(find src -name '*.py' | wc -l | tr -d ' ')
TST_N=$(find tests -name '*.py' | wc -l | tr -d ' ')
SPK_N=$(find spikes -name '*.py' | wc -l | tr -d ' ')
echo "DENOMINATOR src=$SRC_N tests=$TST_N spikes=$SPK_N"
m() { s=$(grep -rEn --include='*.py' "$2" src | wc -l | tr -d ' ')
t=$(grep -rEn --include='*.py' "$2" tests | wc -l | tr -d ' ')
k=$(grep -rEn --include='*.py' "$2" spikes | wc -l | tr -d ' ')
printf "%-4s %-60s src=%-4s tests=%-4s spikes=%s\n" "$1" "$2" "$s" "$t" "$k"
[ "$s" != "0" ] && grep -rEn --include='*.py' "$2" src | head -12; }
m U1 'ConcurrentBuilder|BuildConcurrent|FanIn'
m U1b 'asyncio\.gather|create_task\('
m U2 'WorkflowBuilder|@executor|class .*\(Executor\)|WorkflowContext|SwitchCase|add_edge|add_fan_out'
m U3 'GroupChatBuilder|with_max_rounds|termination_condition|RoundRobinGroupChatManager'
m U4 'Magentic'
m U5 'SkillsProvider|McpSkillsSource|AgentSkillsProvider|agent_framework.*skills'
m U6 'MCPStdioTool|MCPStreamableHTTPTool|MCPWebsocketTool|as_mcp_server'
m U7 'FunctionTool|ai_function|@tool|tools=\['
m U8 'Middleware|middleware='
m U9 'ContextProvider|HistoryProvider|context_providers='
m U10 'AzureAISearch|azure\.search|cosmos|qdrant|redis|VectorStore|vector_store|Qdrant|Chroma|chromadb'
m U11 'AIContextProvider|TextSearchProvider|citation|SourceName|SourceLink|agentic_retrieval|KnowledgeAgent'
m U12 'checkpoint|Checkpoint'
m U13 'approval_mode|request_info|RequestInfoEvent|RequestPort|MagenticPlanReview|ApprovalRequired'
m U14 'opentelemetry|setup_observability|OBSERVABILITY|get_tracer|start_as_current_span'
m U15 'evaluate_workflow|LocalEvaluator|Evaluator|_evaluation|agent_framework.*evaluation'
m U16 'CompactionProvider|SlidingWindow'
m U17 'as_tool\(|AsAIFunction'
m U18 'FoundryChatClient'
m U19 'HarnessAgent|ShellExecutor|FileMemoryStore|ToolApprovalAgent'
```
Rå tellinger: `DENOMINATOR src=32 tests=117 spikes=6` · U1 0/4/4 · U1b 1/5/0 · U2 0/0/0 · U3 10/7/7 ·
U4 0/4/9 · U5 0/0/0 · U5b(`SKILL\.md`) 1/5/0 · U6 6/6/0 · U6b(rå mcp) 7/6/0 · U7 4/10/0 · U8 16/20/0 ·
U9 8/11/4 · U10 2/19/0 · U10b(numpy/cosine) 49/102/1 · U11 26/35/0 · U12 3/0/0 · U13 3/0/0 ·
U14 0/0/1 · U15 0/0/0 · U16 0/0/0 · U17 0/0/0 · U18 7/8/0 · U19 0/0/0.
Kjent-positiv-kontroll mot installert MAF (`grep -rEl <regex> agent_framework agent_framework_orchestrations | wc -l`):
U2 → 21 · U5 → 3 · U14 → 4 · U15 → 2 · U16 → 4 · U17 → 1 · U19 → 1 · `checkpoint_storage|CheckpointStorage`
→ 14 · `MagenticBuilder|StandardMagenticManager` → 4.
B7-/HITL-målingsskriptene (`magentic_b7.py`, `magentic_b7b.py`, `magentic_reset.py`) ligger i
øktens scratchpad og porteres til `spikes/e_magentic.py` i økt 54 (S1/S3) — de committes ikke
herfra, fordi de bruker en ad-hoc `FakeClient` og ikke repoets `ScriptedChatClient`.
## Vedlegg V2 — installert Magentic-API (orchestrations 1.0.0, `_magentic.py`, 1802 linjer)
- **Eksport** (`agent_framework_orchestrations/__init__.py:43-62`): `MagenticBuilder`,
`MagenticContext`, `MagenticManagerBase`, `MagenticOrchestrator`, `MagenticOrchestratorEvent`/
`…EventType`, `MagenticPlanReviewRequest`/`…Response`, `MagenticProgressLedger`/`…Item`,
`MagenticResetSignal`, `StandardMagenticManager`, `MagenticAgentExecutor`. **Ikke i koden:**
`MagenticHumanIntervention*`, `with_human_input_on_stall` (kun docstrings).
- **`MagenticBuilder(*, participants, manager=|manager_factory=|manager_agent=|manager_agent_factory=
(nøyaktig én), task_ledger_*_prompt…, progress_ledger_prompt, final_answer_prompt,
max_stall_count=UNSET→3, max_reset_count=None, max_round_count=None, enable_plan_review=False,
checkpoint_storage=None, output_from, intermediate_output_from)`** (`:1396-1422`). Metoder:
`with_plan_review(enable=True)` (`:1516`), `with_checkpointing(storage)` (`:1564`), `build()`
(`:1772`; bidireksjonale kanter orkestrator↔deltaker `:1796-1797`). Grensene bor på MANAGEREN
(`:1693-1695`, lest tilbake `:905/:1115/:1240`); `manager=` + prompt/limit-argumenter → warning og
**stille ignorert** (`:1661-1677`). Ikke-agent-`Executor` som deltaker aksepteres (`:1498-1499`)
men får placeholder-beskrivelse (`_base_group_chat_orchestrator.py:115`).
- **`StandardMagenticManager(agent, …, max_stall_count=3, max_reset_count=None,
max_round_count=None, progress_ledger_retry_count=None→3)`** (`:528-544`). Én `AgentSession` i
`__init__` (`:572`), alle kall `agent.run(…, session=…)` (`:601`). Ledger-parsing: fri tekst +
`_extract_json` (`:406-449`), dataklasser ikke pydantic (`:288-333`).
- **Løkke:** runde-teller ×2 (`:1075-1076`), grense-sjekk FØR inkrement på `_round_index` (`:1238`);
stall `:1109-1118`; reset `:1149-1185`; **grense → kanonisk streng + `_terminated`, ingen
exception** (`:1222-1256`); sluttsvar `:1206-1220`; ukjent `next_speaker` → stille sluttsvar
(`:1128-1131`); single-use-guard `:914-918`.
- **Plan review:** request `:829-853` (`plan`, `current_progress`, `is_stalled`; `.approve()`,
`.revise(feedback)`), response `:800-826` (`review: list[Message]`, tom = approve `:1010`),
reises `:1041-1048`, svares `workflow.run(responses={id: …})` (`_workflow.py:899-908`; ukjent id
→ `ValueError` `:946`), revise-håndtering `:1014-1031` (ingen cap, ingen runde-telling).
Per-tur-HITL via `AgentApprovalExecutor` (`_orchestration_request_info.py:168`).
- **Checkpoint:** `CheckpointStorage`-protokoll `_checkpoint.py:119`, `InMemoryCheckpointStorage`
`:192`, `FileCheckpointStorage(storage_path, *, allowed_checkpoint_types)` `:239`;
`WorkflowCheckpoint.pending_request_info_events` `:81`; `run(checkpoint_id=, checkpoint_storage=)`
`_workflow.py:701-712`, `responses`+`checkpoint_id` tillatt `:877`; superstep-granularitet
`_runner.py:143-144`; re-emittering `_runner_context.py:414-426`; Magentic-state
`_magentic.py:1266-1284` + manager-sesjon `:746`; feil svelges `:1281-1282`/`:1320-1321`.
- **Observability:** `_magentic.py` → 0 spans. Runtime-spans `workflow.build`/`workflow.run`/
`executor.process`/`edge_group.process`/`message.send` (`observability.py:253-276`).
Inngang `configure_otel_providers(*, enable_sensitive_data, enable_console_exporters, exporters,
views, vs_code_extension_port, env_file_path, env_file_encoding)` (`:1168`), `enable_instrumentation`
(`:1132`); env `ENABLE_INSTRUMENTATION` (default True, `:697`), `ENABLE_SENSITIVE_DATA`,
`ENABLE_CONSOLE_EXPORTERS`, `OTEL_EXPORTER_OTLP_ENDPOINT`/`_PROTOCOL`/`_HEADERS`,
`OTEL_SERVICE_NAME`. Ikke re-eksportert fra `agent_framework` (`hasattr` → False).
`APPLICATIONINSIGHTS_CONNECTION_STRING` → ikke funnet. `opentelemetry-sdk`: **ikke installert**.
- **Evals/Skills:** `evaluate_workflow(*, workflow, workflow_result|queries, evaluators, …)`
(`_evaluation.py:1833`, `@experimental(EVALS)` `:1832`); `SkillsProvider(ContextProvider)`
(`_skills.py:1719`, `@experimental(SKILLS)` `:1718`; `from_paths` `:1861`).
## Vedlegg V3 — kommandoer kjørt (utvalg; fullstendige lister i øktens agent-transkripter)
```
find src -name '*.py' | sort | wc -l # 32
uv pip list | grep -i -E "agent.framework|^mcp |opentelemetry|azure-ai|anyio"
sed -n '61,75p' uv.lock ; grep -n "agent-framework" pyproject.toml
curl -s https://pypi.org/pypi/agent-framework-orchestrations/json # + /1.0.0 … /1.1.1/json
sed -n '1,80p' .venv/.../agent_framework/_feature_stage.py
grep -rn "@experimental|@preview|feature_stage(" .venv/.../agent_framework/*.py .../_workflows/*.py
grep -n -i "experimental|feature_stage" .venv/.../agent_framework_orchestrations/*.py # 0
bash scratchpad/u_measure.sh # V1
grep -rEl <9 null-regexer> .venv/.../agent_framework .venv/.../agent_framework_orchestrations | wc -l
grep -rn -i "research.01" STATE.md STATE-arkiv-2026-08-21.local.md docs/ # 0
grep -rn "enable_layer1_hitl|layer1" src tests ; grep -rEn 'request_info|RequestInfoEvent' tests # 0
grep -rn "agent_framework" src/portfolio_optimiser/*.py # 14 importlinjer
microsoft_docs_fetch learn.microsoft.com/en-us/agent-framework/workflows/orchestrations/magentic
uv run python scratchpad/magentic_b7.py ; … magentic_b7b.py ; … magentic_reset.py # E1E7
.venv/bin/python -c "import opentelemetry.sdk" # ModuleNotFoundError
```