The egress declaration (Trekk B3) says what a run MAY contact. It cannot say what it DID: after the run, nothing distinguished "the agents queried the price register" from "the agents ignored it", and a proposal resting on an external service should be traceable to it. ToolCallRecorder(FunctionMiddleware) mirrors BudgetMiddleware(ChatMiddleware) one layer down — that one observes the debate's chat calls, this one its tool calls. It observes only: call_next is always awaited, so a trace can never alter the run it traces. The record lands on ProvenanceStamp.external_calls, read AFTER the debate so it is a record rather than an intention. MEASURED, not assumed, before any of it was written: FunctionMiddleware fires for a tool served over a REAL MCP stdio subprocess, and context.function.name carries the BARE tool name with no server prefix. That measurement decided the design — MAF cannot tell us which server a tool came from, so attribution comes from our own config, and a name allowed by two servers is recorded UNATTRIBUTED (server="") rather than credited to the first match. Naming a service that may never have been contacted is the one place a guess must not go. Only CONFIGURED tools are recorded. The middleware fires for every function the agents invoke, including the in-process retrieve_cost_docs on the road path; logging those would turn the record into a false egress claim. An empty list is a positive statement — nothing outside this process was contacted — which is why it is always serialized rather than omitted. Honesty limit, written on ExternalCall itself: this is the call and its source. It is NOT evidence that the service's answer reached the proposal, nor a verified rendering of that answer. One finding, and it is the reason for measuring rather than trusting green: the road-path negative test was VACUOUS. Its scripted tool call named an argument the tool does not declare (code vs query), MAF rejected the call before invocation, and the test asserted an empty record against a run where no tool ran at all — green under the exact mutation it existed to catch. It now spies on the recorder and asserts the invocation genuinely reached it before asserting it was not recorded. This is last session's lesson again: a scenario that cannot distinguish two implementations proves nothing. The tool-call double is registered in test_scripted_client_consolidation.py's _DELEGATING_OVERRIDES — it cannot live in the reply_selector seam, which returns a reply STRING, and a response that is not text is its whole subject. Load-bearing MEASURED (tests/test_b4_mcp_call_trace_loadbearing.py) against the whole 755-test suite, four mutations all red: detach the recorder from the debate middleware · record every function invocation · attribute an ambiguous name to the first server · stop reading the recorder into provenance. Control: a run with no configured servers records nothing, so the empty record is a real answer and not the only one the seam can produce. Ran it, not just tested it: the real recorder against a real MCP server subprocess returns ExternalCall(server='prisregister', tool='lookup_unit_price'), and a scripted CLI run's outbox artefact carries the empty list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtRd8y1PDPGwkrRXFhubqr
199 lines
9.5 KiB
Markdown
199 lines
9.5 KiB
Markdown
# Bestille en kjøring
|
|
|
|
Dette dokumentet er for deg som **bestiller** en kjøring — fagpersonen som vet hvilke tilnærminger
|
|
det er verdt å prøve på et prosjekt, og som vil vite nøyaktig hva systemet skal gjøre før det gjør
|
|
det. Søsterdokumentet [ekspert-svar.md](ekspert-svar.md) handler om det motsatte enden: å avgi dommen
|
|
etterpå.
|
|
|
|
> **Ærlighetsmerking.** Bestillingen styrer hva som **vurderes**. Den styrer aldri hva som
|
|
> **godkjennes**. Den deterministiske validatoren gjelder uendret for en tilnærming du selv har bedt
|
|
> om — ber du om noe tallene ikke bærer, blir det avvist, og avvisningen kommer tilbake til deg med
|
|
> begrunnelsen.
|
|
|
|
## Oppdragsfila
|
|
|
|
En bestilling er én JSON-fil. Du gir den til kjøringen med `--mandate`.
|
|
|
|
```json
|
|
{
|
|
"objective": "Kutt energikostnad i BYGG-KONTOR-NORD uten ombygging, med tiltak som kan gjennomføres i 2026.",
|
|
"approaches": [
|
|
{
|
|
"id": "led-retrofit",
|
|
"label": "LED-retrofit av kontorbelysning",
|
|
"description": "Drift mener armaturene er originale og har gått langt forbi oppgitt levetid."
|
|
},
|
|
{
|
|
"id": "driftsavtale",
|
|
"label": "Reforhandling av driftsavtale",
|
|
"description": "Avtalen er ikke justert siden 2019 og dekker areal vi ikke lenger disponerer."
|
|
}
|
|
],
|
|
"allow_own_proposals": true,
|
|
"success_criteria": "Minst ett tiltak som passerer validatoren og som drift kan stå inne for."
|
|
}
|
|
```
|
|
|
|
| Felt | Hva det gjør |
|
|
|---|---|
|
|
| `objective` | Hva kjøringen er til for, i klartekst. **Påkrevd** — en kjøring uten uttalt hensikt er nettopp det denne fila finnes for å hindre. |
|
|
| `approaches` | Tilnærmingene du vil ha vurdert. Hver får sin egen vurdering og sin egen linje i oppgjøret. |
|
|
| `allow_own_proposals` | `true` (standard): systemet foreslår også sitt eget, i tillegg til dine. `false`: kun dine. |
|
|
| `success_criteria` | Hva du vil regne som vellykket. Gjengis ordrett i kunngjøringen; ingenting tolker den maskinelt. |
|
|
|
|
**Om `id`:** den er nøkkelen tilnærmingen rapporteres under. To tilnærminger kan ikke dele `id`
|
|
(da ville de kollapset til én linje i oppgjøret), og `own-proposal` er reservert for systemets eget
|
|
forslag.
|
|
|
|
**Om `description`:** den mates ordrett inn til modellen. Det er her fagkunnskapen din faktisk
|
|
gjør en forskjell — *hvorfor* du tror tilnærmingen er verdt å prøve er nøyaktig det systemet ikke
|
|
kan lese seg til av kostnadstallene.
|
|
|
|
**Enten tilnærminger eller egne forslag.** En fil med tom `approaches` **og**
|
|
`allow_own_proposals: false` blir avvist: da har kjøringen ingenting å gjøre.
|
|
|
|
## Å kjøre den
|
|
|
|
```bash
|
|
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
|
|
--docs-dir shared/examples/bygg-energi-mikro \
|
|
--bundle-dir shared/examples/bygg-energi-mikro \
|
|
--mandate oppdrag.json \
|
|
--scripted-replies replies.json
|
|
```
|
|
|
|
`--mandate` virker i begge moduser. I porteføljemodus (`--portfolio`) gjelder bestillingen **hvert**
|
|
prosjekt i passet, og hvert prosjekt gjør opp for seg.
|
|
|
|
**Vil du se hva kjøringen ville gjort, uten å bruke penger?** Legg til `--live-dry-run`. Da skrives
|
|
kunngjøringen, og kjøringen stopper før første modellkall.
|
|
|
|
**Fila leses strengt.** Mangler den, eller er den feilformet, blir kjøringen **nektet** (rc 1) — den
|
|
går aldri videre på en halvlest bestilling. Det er med vilje: et oppgjør som beskriver arbeid ingen
|
|
har bestilt, er verre enn ingen bestilling.
|
|
|
|
## Hva du får: kunngjøringen
|
|
|
|
Før noe koster penger, skriver kjøringen hva den skal gjøre:
|
|
|
|
```
|
|
Run mandate for BYGG-KONTOR-NORD
|
|
Objective: Kutt energikostnad i BYGG-KONTOR-NORD uten ombygging, ...
|
|
Evaluates: 2 expert-proposed approach(es) + the system's own proposals
|
|
1. led-retrofit — LED-retrofit av kontorbelysning
|
|
2. driftsavtale — Reforhandling av driftsavtale
|
|
Stops at: 3 rounds / 100000 tokens
|
|
Contacts: no external services
|
|
Success: Minst ett tiltak som passerer validatoren ...
|
|
```
|
|
|
|
Kjører du med `--dimension-config`, kommer det en `Scoped to:`-linje til. Linjer for ting kjøringen
|
|
ikke har — avgrensning, tallmål — utelates i stedet for å stå tomme: en tom linje ville lest som en
|
|
beslutning ingen har tatt.
|
|
|
|
`Contacts` er egress-erklæringen: en kjøring når aldri en ekstern tjeneste den ikke har navngitt
|
|
her. `Stops at` er takene — systemet kjører aldri en ubegrenset løkke.
|
|
|
|
## Hva du får: oppgjøret
|
|
|
|
Etter kjøringen svarer den for bestillingen, én linje per tilnærming:
|
|
|
|
```
|
|
Mandate outcome
|
|
led-retrofit VALIDATED 30000 NOK LED-retrofit av kontorbelysning
|
|
driftsavtale REJECTED claimed 240000 exceeds feasible 90000
|
|
own-proposal NOT EVALUATED budget exhausted before this approach was evaluated
|
|
Validated: 1 of 3 approaches — best 30000 NOK (the outcome this run carries)
|
|
```
|
|
|
|
De tre statusene betyr forskjellige ting, og forskjellen er poenget:
|
|
|
|
- **VALIDATED** — tallene holdt. Beløpet er det validatoren slapp gjennom.
|
|
- **REJECTED** — tilnærmingen ble vurdert, og falt på tallene. Grunnen står der; det er den du kan
|
|
gjøre noe med (ofte er den «regnet mot feil grunnlag», ikke «dårlig idé»).
|
|
- **NOT EVALUATED** — kjøringen rakk den aldri. Den er **ikke** avvist, og den er ikke glemt.
|
|
|
|
En tilnærming du bestilte forsvinner aldri i stillhet. Det er hele grunnen til at oppgjøret finnes.
|
|
|
|
**Tilnærmingene summeres ikke.** Flere av dem angriper som regel samme kostnadslinje, så de er
|
|
alternativer — ikke besparelser som legges oppå hverandre. Derfor står det *hvor mange som holdt*
|
|
og *hvilken kjøringen bærer videre*, aldri en sum. (Dette ble oppdaget ved å faktisk kjøre den:
|
|
tre tilnærminger mot samme linje ga «totalt 90 000», som ingen av dem kunne innfri.)
|
|
|
|
## Hver tilnærming kan dømmes for seg
|
|
|
|
Kjører du med `--outbox-dir` (og `--run-id`), legger kjøringen igjen **én artefakt per vurdert
|
|
tilnærming** — `run-001-led-retrofit-proposal.json`, `run-001-driftsavtale-proposal.json`, og så
|
|
videre — ikke bare for den kjøringen bar videre.
|
|
|
|
Det er ikke en bokføringsdetalj. Uten det kunne bare den valgte tilnærmingen få en fagdom, og de
|
|
andre du bestilte ville aldri nådd læringssløyfa: de ble vurdert, rapportert i oppgjøret, og deretter
|
|
glemt. Nå står de hver for seg i køen din:
|
|
|
|
```
|
|
uv run python -m portfolio_optimiser.hitl pending --outbox-dir utboks --verdict-dir innboks
|
|
run-001 c91cb2fe1aa139a9 validated [led-retrofit]
|
|
run-001 4f0a1d77b2e5c318 rejected [driftsavtale]
|
|
```
|
|
|
|
Hvordan du svarer på dem står i [ekspert-svar.md](ekspert-svar.md). En tilnærming med status
|
|
NOT EVALUATED har ingen artefakt — kjøringen produserte aldri et forslag for den, så det er ingenting
|
|
å dømme.
|
|
|
|
## Hva bestillingen ikke gjør
|
|
|
|
- **Den overstyrer ikke validatoren.** Se ærlighetsmerkingen øverst.
|
|
- **Den avgjør ikke om et tiltak skal gjennomføres.** Det gjør et menneske, etterpå — se
|
|
[ekspert-svar.md](ekspert-svar.md).
|
|
- **Den setter ikke et tallmål.** Sparemålet har ett hjem, målkonfigurasjonen (`--goals`, som gjelder
|
|
porteføljemodus). To kopier av samme tall drifter fra hverandre, så oppdraget bærer hensikten i ord
|
|
og gjengir tallet hvis det finnes.
|
|
- **Den lagrer seg ikke som kunnskap.** Bestillingen er en instruks for én kjøring. Det som lærer
|
|
systemet noe, er dommen du avgir etterpå.
|
|
|
|
## Hvis kjøringen skal spørre en ekstern tjeneste
|
|
|
|
Skal agentene kunne slå opp i en tjeneste dere allerede har — et prisregister, målerdata, et
|
|
avtaleregister — settes den opp med `--mcp-config`:
|
|
|
|
```json
|
|
{"servers": [
|
|
{"name": "prisregister", "transport": "http", "url": "https://intern.example/mcp",
|
|
"allowed_tools": ["lookup_unit_price"], "timeout_seconds": 15,
|
|
"credential_env": "PRISREGISTER_TOKEN"}
|
|
]}
|
|
```
|
|
|
|
Tre ting er verdt å vite som bestiller:
|
|
|
|
- **Du må navngi hvilke verktøy som er tillatt.** Tom liste er ikke lov. Uten det ville tjenesten
|
|
selv bestemt hva agentene får lov til å kalle.
|
|
- **Alt som vil bli kontaktet, står i kunngjøringen** — før første kall, på `Contacts:`-linja, med
|
|
både servernavn og tillatte verktøy. En kjøring når aldri en tjeneste den ikke har navngitt. Det
|
|
gjelder også når du ikke bruker `--mandate`.
|
|
- **Passord og nøkler skal aldri i fila.** `credential_env` navngir en miljøvariabel; verdien leses
|
|
derfra. Er variabelen ikke satt, blir kjøringen nektet — heller det enn å ringe tjenesten anonymt
|
|
og få svar med feil tilgang.
|
|
|
|
Uten `--mcp-config` gjøres ingen nettverkskall i det hele tatt.
|
|
|
|
### Etterpå: hva ble faktisk kalt
|
|
|
|
Kunngjøringen er en **tillatelse** — den sier hva kjøringen *kan* kontakte. Den sier ikke om
|
|
agentene brukte tjenesten eller lot den ligge. Derfor fører kjøringen også et **kall-spor**, som
|
|
følger med artefaktene under `provenance.external_calls`:
|
|
|
|
```json
|
|
"external_calls": [{"server": "prisregister", "tool": "lookup_unit_price"}]
|
|
```
|
|
|
|
En tom liste er en påstand, ikke et hull: ingenting utenfor prosessen ble kontaktet. Verktøy som
|
|
kjører lokalt (som dokumentsøket) telles ikke med — de er ikke kontakt med noen.
|
|
|
|
**Hva sporet ikke sier.** Det viser at verktøyet ble kalt, og hvilken server det tilhører. Det er
|
|
ikke bevis for at tjenestens svar er det tallet som havnet i forslaget, og det er ingen kontrollert
|
|
gjengivelse av hva tjenesten svarte. Skal svaret etterprøves, må det gjøres mot tjenesten selv.
|
|
|
|
En sjelden detalj, tatt med fordi den ellers ville sett ut som en feil: hvis to servere tilbyr
|
|
verktøy med **samme navn**, står `server` tomt. Rammeverket oppgir bare verktøynavnet, så de to lar
|
|
seg ikke skille — og da er «vet ikke» riktigere enn å gjette på den ene.
|