feat(cli): console entry points + the demo's stderr damping (P4 pkt. 5 og 2)
Two commands are now part of the install surface a fresh clone gets from `uv sync`: `portfolio-optimiser` (run:main) and `portfolio-optimiser-demo` (simulation:main). Deliberately two of five main()s — costsim/hitl/preflight stay module-invoked; every name here is a name the freeze has to carry. Pinned against the INSTALLED distribution's metadata, not the TOML: a [project.scripts] line that has never been synced is a claim, not a command. Measured: stdout is byte-identical across both invocation forms. stderr (P4 pkt. 2), the session's open decision, resolved by measurement rather than by preference. Damped: the round-cap notice only, via a filter on the emitting logger, keyed on the message and installed by main() — never at import, so a library consumer keeps its own logging config. NOT damped: the two ExperimentalWarnings. They fire while the package __init__ imports run -> agent_framework, always before simulation's own imports and under both invocation forms, so silencing them would mean filtering warnings inside the library package on every consumer's behalf; they are pinned in pkt. 3 instead. A console-script wrapper was rejected for a second reason: the two forms would then write different stderr, and a byte-fasit would pin the command rather than the program. stderr 6 -> 4 lines. A first implementation wrapped simulation's own agent_framework import in a scoped mute. Measurement showed it can never fire — the package __init__ has already imported agent_framework by then — so it was removed rather than left as a green-but-dead seam. Load-bearing MEASURED against the whole suite, five mutations all red + green control: remove [project.scripts] · typo the target · detach the main() call · make the filter drop everything · install the filter at import time. The typo mutation also felled a test: the resolve-assert re-checked the expected constant against itself, and now resolves what the distribution actually installs. 775 -> 785 passed / 4 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2bxLcCRguxXzpM4priTMn
This commit is contained in:
parent
1522e2aaaa
commit
ab7f45aa95
6 changed files with 298 additions and 2 deletions
|
|
@ -21,6 +21,7 @@ client is MAF-side scaffolding; it is NOT part of the framework-neutral ``shared
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -54,6 +55,56 @@ from portfolio_optimiser.verdicts import (
|
|||
|
||||
_PROJECT_ID = "BYGG-KONTOR-NORD"
|
||||
|
||||
# --- P4 pkt. 2: demo stderr discipline -----------------------------------------------------------
|
||||
# Measured 2026-08-09, the demo wrote six stderr lines: two ``ExperimentalWarning``s from
|
||||
# ``agent_framework``, two round-cap notices from the orchestrator, a blank line and the
|
||||
# ``arbeidskopi:`` path. The last two are ours and stay (the path is the one deliberately
|
||||
# non-deterministic value, which is exactly why it is on stderr and not on stdout).
|
||||
#
|
||||
# **Damped here: the round-cap notices only.** They are emitted DURING the demo's own run, by an
|
||||
# event the demo deliberately provokes — the maker/checker debate is configured to run to its cap.
|
||||
#
|
||||
# **NOT damped: the two ``ExperimentalWarning``s — they are pinned in pkt. 3 instead.** Measured,
|
||||
# not assumed: they fire while ``portfolio_optimiser/__init__.py`` imports ``run``, which imports
|
||||
# ``agent_framework`` — always before this module's own import block, under BOTH invocation forms.
|
||||
# Silencing them would therefore mean putting a warnings filter inside the library package, i.e.
|
||||
# letting this framework decide what MAF is allowed to tell every consumer that imports it. That is
|
||||
# a library-behaviour change for stderr nobody projects, days before a freeze. A wrapper that
|
||||
# muted them only behind the console script was rejected for a second reason: it would make
|
||||
# ``uv run portfolio-optimiser-demo`` and ``uv run python -m portfolio_optimiser.simulation`` write
|
||||
# different stderr, and a byte-fasit would then pin the command rather than the program.
|
||||
#
|
||||
# The damping that IS here is NARROW by construction, which is the load-bearing part: a different
|
||||
# orchestration warning still reaches stderr and trips the pkt. 3 pin. A filter that could only
|
||||
# ever say "drop" would leave a pin that can no longer fail for the reason it exists.
|
||||
|
||||
#: The logger that emits the round-cap notice, read out of MAF's source (``logger.warning`` in
|
||||
#: ``_base_group_chat_orchestrator``), not guessed. Logger filters apply only to the logger the
|
||||
#: record was logged THROUGH — an ancestor's filters are never consulted — so this must be the
|
||||
#: emitting module's own name. If MAF moves the call, the notice simply reappears on stderr and the
|
||||
#: pkt. 3 pin says so; the failure mode is visible, not silent.
|
||||
ROUND_CAP_LOGGER = "agent_framework_orchestrations._base_group_chat_orchestrator"
|
||||
|
||||
|
||||
class _ExpectedRoundCapFilter(logging.Filter):
|
||||
"""Drops ONLY the "round limit reached" notice, which the demo reaches by design (the
|
||||
maker/checker debate runs to its cap). Keyed on the event, not on the configured number: the
|
||||
cap is a demo setting, while the notice is the thing we have decided is expected."""
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
message = record.getMessage()
|
||||
return not ("reached max_rounds=" in message and "forcing completion" in message)
|
||||
|
||||
|
||||
def quiet_expected_round_cap_notice() -> logging.Filter:
|
||||
"""Install the round-cap filter on the emitting logger and return it (so a caller — a test —
|
||||
can remove it again). Runtime state, installed by ``main()``: importing this module must not
|
||||
reconfigure logging for a library consumer."""
|
||||
installed = _ExpectedRoundCapFilter()
|
||||
logging.getLogger(ROUND_CAP_LOGGER).addFilter(installed)
|
||||
return installed
|
||||
|
||||
|
||||
# --- Step 7, the LONG loop: what an expert drops into the inbox between the two runs -------------
|
||||
# A SECOND marker, deliberately distinct from the persona's (Step 8, promotion). The two mechanisms
|
||||
# both end in Run B's hypothesis prompt, so a single shared marker would let either path carry it
|
||||
|
|
@ -673,6 +724,11 @@ def main(argv: list[str] | None = None) -> int: # pragma: no cover - console tr
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
# P4 pkt. 2: runtime half of the stderr discipline (the import-time half wrapped the
|
||||
# ``agent_framework`` import above). Installed here rather than at import time so that a
|
||||
# library consumer of this module keeps its own logging configuration.
|
||||
quiet_expected_round_cap_notice()
|
||||
|
||||
work = tempfile.mkdtemp(prefix="po-sim-")
|
||||
# THE call site (P4 pkt. 0): the demo runs the ANCHORED reserve — the shared bundle plus the
|
||||
# cost baseline it cannot be given in place. On GO day these two lines point at the delivered
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue