Repository navigation
Add the typed microcosm-emitter-interface package and release it with the emitter - #22
Merged
Merged
Conversation
anth-volk
added this pull request to stack #25
October 19, 2026 21:21
This was referenced Oct 19, 2026
anth-volk
force-pushed
the
feat/emitter-interface-package
branch
from
October 20, 2026 12:56
c59d938 to
5b0ff54
Compare
`microcosm-emitter-interface` is a dependency-free distribution, built from this repository, that Microcosm will depend on with a version range. - `start_emitter` runs the installed `microcosm-emitter` command (the `command` argument, then MICROCOSM_EMITTER_COMMAND, then PATH) as the build's own child process with no `env` argument, so the emitter inherits the build's environment and user and finds the Hugging Face credential in the same order as core/auth.py. - After readiness it sends `hello` and refuses an emitter that does not accept PROTOCOL_VERSION; startup errors carry only a fixed reason. - `EmitterClient` offers typed telemetry events, sent as exactly the messages of the emitter's in-process client, and Orrery input hand-overs. No call raises because the emitter is missing or refusing. - The emitter depends on the interface release of the same version, as a uv workspace member. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interface package now ships `fixtures/protocol-v1/`: one JSON file per message it sends (telemetry events, Orrery queries, module configurations and the host's `hello` answer). - The interface's tests check that each typed call sends exactly its fixture, and that every fixture has such a call. - The emitter's contract tests check that every fixture uses a protocol version in SUPPORTED_PROTOCOL_VERSIONS, that the telemetry and Orrery modules and the installed command accept each one, and that released fixtures keep their recorded SHA-256. - The interface README states the compatibility rule: every emitter release accepts every protocol version still in use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `tools/bump-version.py` bumps the version line in both pyproject.toml files and the emitter's exact `microcosm-emitter-interface` pin, and refuses diverged versions or a pin that differs from the version. - `tools/check-artifacts.sh` builds both distributions with `uv build --all-packages`. - `tools/check-installed.py` installs the emitter with its interface into one clean environment, as `uv tool install` would, and the interface alone into another, as a build's environment would. From the second it starts the first's `microcosm-emitter` command and sends telemetry and an Orrery hand-over (`tools/installed-interface-smoke.py`). - Import-boundary tests: the interface imports only the standard library and has no import side effects, and the emitter host does not import it. - README and push.yaml describe publishing both distributions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --all-packages, uv's --clear empties the output folder for each package's build, so one build could delete the other's artifacts (seen locally as a build failing on a missing dist/.gitignore). The artifact check now removes dist once and builds both packages into it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`EmitterClient.emit_telemetry` converted its command (`telemetry_message`, then `wire_value`) before checking that an emitter was running and outside the handler for `LocalEmitterError`; `EmitterClient.start` called `TelemetrySettings.configuration` before its `try`. A multi-element NumPy array or tensor (whose `.item()` raises), a self-referencing mapping or a deeply nested value then raised into the build, even with no emitter running. The emitter's in-process client warns and returns instead. - Every call now builds its message lazily, inside its error handling, after checking that the emitter loaded the call's module. A conversion failure returns False (or an unavailable receipt with error code `invalid_arguments`), records `RequestFailure(kind, reason, error_type)` in `EmitterClient.last_failure` and warns once per kind of call with the reason and only the error's type name. - An unexpected error from the transport is reported as `not_accepted`. - Startup settings or a spool path that cannot be converted start an unavailable client with the reason `invalid_configuration`. - `hand_over_orrery_inputs` no longer indexes an inventory without a publication identifier, and an unusable `wait_seconds` means no wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish job uploaded every file in `dist/` in one `pypa/gh-action-pypi-publish` step without `skip-existing`, and the emitter's files sort before the interface's. If the interface upload failed (for example before its trusted publisher exists), PyPI held `microcosm-emitter==X`, which requires a `microcosm-emitter-interface==X` that does not exist, and rerunning the job stopped on "File already exists". - `ci.yml`'s build-packages job also uploads each distribution as its own artifact (`interface-distribution`, `emitter-distribution`); the combined `distributions` artifact stays for the collector job. - The publish job downloads and uploads the interface first, then the emitter, each with `skip-existing: true`, so a failed interface upload leaves no emitter on PyPI and a rerun uploads only the missing files. No environment variables or secrets change. The publish job still uses PyPI trusted publishing (`id-token: write`, environment `pypi`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ures
The fixtures recorded each module's message and settings, but not the
arguments the launcher passes to `microcosm-emitter host`, the configuration
object it writes to standard input, or the envelope of each socket request.
An argument-parser or envelope change made on both sides would therefore
pass every test while breaking builds that still run an older interface.
- New protocol-v1 fixtures: `host-launch` (arguments and standard input),
and the envelopes `host-ping`, `host-message-envelope`,
`host-query-envelope` and `host-close`. The loader gains the `launch` and
`envelope` kinds, an envelope's expected `response`, and the
`{socket_path}` and `{parent_pid}` placeholders.
- Interface tests check that the launcher and the transport send exactly
these fixtures; the emitter's contract tests start the installed command
from the launch fixture and send each envelope to a host.
- The `hello` contract test now requires only the recorded keys, so an
emitter that adds a field still passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitter's in-process telemetry and Orrery clients duplicated the interface's run identity, value conversion, receipts and messages, and their error handling had already diverged: the in-process Orrery client resolved the directory and indexed the inventory outside its error handling, and let unexpected transport errors raise. The emitter depends on the interface of the same version, so it now reuses it: - `microcosm_emitter.telemetry`: `wire_value`, `TelemetryRun`, the event and status types and the default failure class and heartbeat come from `microcosm_emitter_interface.telemetry`. `LocalTelemetryEmitter` builds its configuration with `TelemetrySettings` and each message with the interface's typed commands and `telemetry_message`, inside its existing error handling. - `microcosm_emitter.orrery`: the publication shapes, `default_spool_path` (now `microcosm_emitter_interface.spool`), the unavailable receipt and the four Orrery messages come from `microcosm_emitter_interface.orrery`, which also gains `publication_id_of`, `answered_inventory` and `answered_receipt`. `GraphPublicationClient` builds each message inside its error handling and reports `invalid_arguments` instead of raising. Existing import paths keep working as re-exports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After an unrelated call fails with invalid_arguments, a receipt request answered without a receipt reports invalid_arguments instead of local_service_unavailable, which the in-process client returns for the same answer. Red: 2 failed, 12 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EmitterClient read the shared last_failure to choose an unavailable receipt's error code, and never cleared it after a success. A receipt request answered without a receipt then reported invalid_arguments whenever any earlier call had failed that way, or another thread's call had. _request now returns the answer together with this call's failure reason, and _query keeps its signature for every other caller. The Orrery receipt calls take the code from that reason, so the interface and the in-process client return the same receipt for the same answer. Green: 14 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hello answer whose module names are not strings passed the launcher's
check, and every later call then raised TypeError while building the
set of module names. An Orrery answer whose receipt had no status made
the waiting loops raise KeyError. Both reach the build only from a
non-conforming emitter, but no call may raise into the build.
Red: 4 failed, 26 passed. The three hello answers were accepted ("DID
NOT RAISE EmitterStartupError"); the receipt raised "KeyError:
'status'".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The launcher refuses a hello answer unless its protocol versions are integers and its module names are strings (unsupported_protocol_version), so a later emitter's differently shaped answer stops the client at startup instead of raising in every call. - answered_receipt treats a receipt without a string status as no receipt. The interface and the in-process Orrery client share it, so both return the same unavailable receipt and their waiting loops never read a missing status. Green: 30 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interface README said the two packages "are versioned separately in practice", but the release tooling gives both one version and pins the emitter to the same interface release. It now says that, and that a build can still run with a different installed emitter, which is why the socket protocol has its own version. Documentation only; no red step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
feat/emitter-interface-package
branch
from
October 20, 2026 22:47
26f9f04 to
03a3c2e
Compare
Brings in #21's release commit `8b3aa29`: both version lines of the emitter's pyproject.toml and uv.lock move to 0.4.0, CHANGELOG.md gains the 0.4.0 entry, and #21's consumed changelog fragment is deleted. No conflicts. The interface package's own version and the emitter's exact pin on it are still 0.3.0; the next commit sets them to 0.4.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#21 released 0.4.0, so main's emitter is at 0.4.0 while this branch's interface package and the emitter's exact pin on it were still 0.3.0. `tools/bump-version.py --check` refused that ("Both distributions must have the same version"), and tests/test_package_names.py would fail on CI. Both are now 0.4.0, and `uv lock` records the interface at 0.4.0. With this branch's `.added` fragment, the release this merge starts publishes both distributions as 0.5.0 (`bump-version.py --check` prints "0.4.0 -> 0.5.0"). Checked: `bump-version.py --check` exits 0; tests/test_package_names.py, tests/test_workflows.py and tests/interface/test_launcher.py pass (46). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md said to publish only the `microcosm-emitter` distribution, which this PR contradicts by adding `microcosm-emitter-interface`. The rule now names both distributions, says they are released together at one version (`microcosm-emitter==X` requires `microcosm-emitter-interface==X`), and still forbids standalone telemetry packages, optional telemetry installation and any further distribution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18
Second of four stacked pull requests (base: #21). Adds
microcosm-emitter-interface, the small typed package Microcosm will depend on with a version range instead of pinning the emitter. Microcosm decides what to hand over; the emitter decides where it goes.What changes
A second distribution,
packages/microcosm-emitter-interface/(import namemicrocosm_emitter_interface), with no dependencies outside the standard library:launcher.start_emitterruns the installedmicrocosm-emittercommand — thecommandargument, thenMICROCOSM_EMITTER_COMMAND, thenPATH— as the build's own child process:subprocess.Popenwith noenvargument, configuration on standard input,--parent-pidset to the build's PID. After the readiness ping it sendshelloand refuses an emitter that does not list itsPROTOCOL_VERSION(unsupported_protocol_version). Startup errors carry only a fixed reason and exit code.EmitterClientoffers typed calls: telemetry events (client.telemetry.stage,transition_stage,progress,calibration_progress,fail,refuse,complete,emit), sent as exactly the messages of the emitter's in-process client, and Orrery input hand-overs (orrery_inventory,hand_over_orrery_inputs,orrery_receipt,retry_orrery_inputs) on the existing Orrery module. No call raises into the build: not because the emitter is missing, slow or refusing, and not because an argument cannot be converted to JSON. Each call converts its arguments inside its error handling, reports whether it was accepted, and records why not inEmitterClient.last_failure(emitter_unavailable,invalid_argumentsornot_accepted).blocking_gate_ids,blocking_failure_countandgate_statuses(the identifiers of the validation checks that refused the build, how many failed, and each check's status) are filled fromrefuse(refusing_check_ids=…, refusing_check_count=…, check_statuses=…).fixtures/protocol-v1/*.json, shipped in the wheel: one per message the package sends.Compatibility rule (interface README): every emitter release must accept every protocol version that interface releases still in use send. Adding a module or message keeps the version; changing a message's shape needs a new version. Tests enforce it from both sides:
host-launch.json: its arguments and the configuration object on standard input) and each socket request's whole envelope (host-ping,host-message-envelope,host-query-envelope,host-close,host-hello), so an argument-parser or envelope change made on both sides cannot pass unnoticed;SUPPORTED_PROTOCOL_VERSIONS, that the telemetry and Orrery modules accept each message fixture, that the installed command starts from the launch fixture and a host answers each envelope, that ahelloanswer has at least the recorded keys (a newer emitter may add fields), and that released fixtures keep their recorded SHA-256 (a released fixture is never edited, only added).The emitter's in-process telemetry and Orrery clients now build their run identity, configuration, messages and receipts with the interface's code (re-exported at their old import paths), so the two kinds of client cannot drift apart.
Credential detection is preserved.
tests/interface/test_launcher.py::test_child_finds_the_build_hugging_face_credential_in_the_same_orderstarts the real installed command through the interface with the fake collector on loopback. It sets the credential only in the test process's environment (three cases:HF_TOKENandHUGGINGFACE_TOKENand a saved login;HUGGINGFACE_TOKENand a saved login; a saved login only) and checks the bearer token of the child's token exchange:HF_TOKENfirst, thenHUGGINGFACE_TOKEN, then the saved login. Another test checks the child's parent is the build process and its user is the build's user. All tokens are obvious fakes.Packaging and release automation
microcosm-emitter-interface==<same version>(a uv workspace member), so the emitter always runs the checks and fixtures of its own release. The two packages share one version.tools/bump-version.pybumps bothpyproject.tomlversion lines and the exact pin, and refuses diverged versions or pins.tools/check-artifacts.shbuilds both withuv build --all-packagesinto adist/it clears once beforehand (uv's--clearempties the shared folder for each package, so one build could delete the other's files; seen locally).tools/check-installed.pyinstalls the emitter with its interface into one clean environment, asuv tool installwould, and the interface alone into a second, as a build's environment would; from the second it starts the first's command and sends telemetry and an Orrery hand-over (tools/installed-interface-smoke.py).ci.yml's build-packages job also uploads each distribution as its own artifact (interface-distribution,emitter-distribution).push.yaml's publish job uploads the interface first, then the emitter, each withskip-existing: true: if the interface upload fails, the emitter is not uploaded, and rerunning the job uploads only the missing files instead of stopping on "File already exists".Release
Fragment type
added: merging releases the next minor version of both distributions, 0.5.0 (tools/bump-version.py --checkprints "0.4.0 -> 0.5.0" on99cf9e2).Done on 2026-10-21 (
d8a95e9,99cf9e2): required step once #21 merges. #21 released 0.4.0 (release commit8b3aa29, on PyPI at 02:11 UTC).d8a95e9mergesmaininto this branch without conflicts, and99cf9e2sets the interface version and the exact pin to 0.4.0 and re-runsuv lock. Checked:bump-version.py --checkexits 0;tests/test_package_names.py,tests/test_workflows.pyandtests/interface/test_launcher.pypass (46). The original instruction: merging #21 movesmainto a new version: 0.4.0, or a later one if #29 merges first. This branch's interface version (packages/microcosm-emitter-interface/pyproject.toml) and the emitter's exact pin (microcosm-emitter-interface==…inpyproject.toml), both 0.3.0 now, must then be set tomain's version, anduv lockrun again; #23, #24 and #27 inherit the change. Otherwisetests/test_package_names.pyfails andtools/bump-version.py --checkrefuses ("Both distributions must have the same version"), so the required check blocks the merge and no release can be made from this branch.Done on 2026-10-21 (
089ae52):AGENTS.md's publishing rule. It said to publish onlymicrocosm-emitter, which this PR contradicts. It now names both distributions, says they are released together at one version (microcosm-emitter==Xrequiresmicrocosm-emitter-interface==X), and still forbids standalone telemetry packages, optional telemetry installation and further distributions.Done on 2026-10-21 (reported by the maintainer; pending publishers aren't publicly visible, so not checked here): manual step before merging. Add a pending trusted publisher on PyPI for the new project
microcosm-emitter-interface(ownerPolicyEngine, repositorymicrocosm-emitter, workflowpush.yaml, environmentpypi). The name is free on PyPI today. Without it, the release run's publish job fails at the interface upload, before uploading the emitter; once the publisher exists, rerunning the job completes the release.Environment variables and secrets (release automation changed)
APP_ID,APP_PRIVATE_KEYpush.yamlversioningjobsecrets.*in thecreate-github-app-tokensteppush.yamlpublishjob, both upload stepsid-token: write, environmentpypi; checked 2026-10-20: the environment's only branch policy ismain)microcosm-emitter(it published 0.4.0 on 2026-10-21); formicrocosm-emitter-interface, the pending publisher was added on 2026-10-21, and its first upload, at this PR's release, is what proves it (the project is not on PyPI yet: its JSON API answers 404)ACTIONS_RUNTIME_TOKEN,ACTIONS_RESULTS_URL)ci.ymlupload steps,push.yamldownload stepsinterface-distributionandemitter-distribution, which theverifyjob's build-packages job uploads in the same runTEST_DATABASE_URLci.ymlcollectorjobenvMICROCOSM_EMITTER_COMMANDcheck-installed.pyremoves itHF_TOKEN,HUGGINGFACE_TOKEN,HF_TOKEN_PATH,HF_HOMEcheck-installed.pyremove the tokens and pointHF_HOME/HF_TOKEN_PATHat empty temporary pathsValidation
Red-green by commit (only the test files this pull request adds or changes):
fb40e3fAdd the typed interface package that starts the installed emittertests/interface/test_launcher.py: 12 of 12 fail (package missing).tests/test_package_names.pywith the oldpyproject.toml: 3 failed, 1 passed1b1c32eShip protocol fixtures and test the emitter against themtest_client_messages.py,test_protocol_fixtures.py: 32 of 32 fail (fixtures and loader missing)f28c8f7Build, verify and release both distributions togethertests/test_workflows.py,tests/test_boundaries.py: 7 failed, 25 passed (the 3 new import-boundary tests already held)bash tools/check-artifacts.shexit 0; re-run: 32 passedf7374aaClear the shared artifact folder once before building both packagestests/test_workflows.py: 1 failed, 23 passedcheck-artifacts.shexit 0 three runs in a row; re-run: 24 passedruff check,ruff format --checkandbash -n tools/*.shpass.Notes for review
microcosm-emitterdistribution". This pull request adds a second distribution because the plan calls for an interface package published from this repository. I have not edited AGENTS.md; if this direction is accepted, its rule could read: "Publishmicrocosm-emitterand the dependency-freemicrocosm-emitter-interface; do not introduce standalone telemetry packages or optional telemetry installation."microcosm-emitter-interface>=<first version with the calls it uses>,<1, since the repository's release tooling makes breaking changes a major bump.pyproject.toml(dev dependencies) anduv.lock; whichever merges second re-runsuv lock. Keep one build's telemetry login from deciding other builds' runs #5–Say what the collector refused, fit requests to its limit, register a lost run again #7 also editREADME.md.Review fixes (2026-10-20)
Each fix was written red, then green; only the test files this pull request adds or changes were run.
5a27b36Never let a hand-over's arguments raise into the buildemit_telemetryconverted its command before checking availability and outside theLocalEmitterErrorhandler, andstartconvertedTelemetrySettingsbefore itstry: a multi-element NumPy array or tensor, a self-referencing mapping or deep nesting raised into the build even with no emitter running.tests/interface/test_client_errors.py: 12 failedtests/interface: 56 passeddcdfb42Publish the interface before the emitter, and let a rerun finishpypa/gh-action-pypi-publishstep withoutskip-existinguploaded the emitter's files first, so a failed interface upload leftmicrocosm-emitter==Xrequiring a missing interface, and a rerun stopped on "File already exists".tests/test_workflows.py: 3 failed, 24 passedactionlint1.7.12 clean07327b1Record the command's launch and the socket envelopes as protocol fixtureshellotest failed on an added field.test_host_protocol.py,test_protocol_fixtures.py,test_client_messages.py: 5 failed, 36 passed (the envelope tests passed already: they record what the transport sends)5b0ff54Build the in-process clients' messages with the interface's codetests/interface/test_in_process_clients.py: 3 failed, 13 passed (the ten message-equivalence checks passed already)tests/telemetry/test_client.py,tests/orrery/test_client.pyandtests/test_boundaries.py: 135 passed;tools/check-artifacts.shexit 0Re-run after the last commit:
tests/interface,tests/test_workflows.py,tests/test_boundaries.py,tests/test_package_names.py: 120 passed. The other commits' hashes above changed when this branch was restacked on #21.Review fixes, second round (2026-10-20)
Each fix is a commit that adds a test failing because of the bug, then a commit with the code. Only the test files this pull request adds or changes were run.
e5dbbbe→3f6fc41Report each Orrery receipt's error code from its own callinvalid_arguments, an Orrery receipt request answered without a receipt reportedinvalid_arguments: the code came from the sharedlast_failure, which is never cleared (introduced by5a27b36). The emitter's in-process client reportslocal_service_unavailablefor the same answer._requestnow returns the call's own failure reason;_querykeeps its signature.tests/interface/test_client_errors.py: 2 failed, 12 passed (assert 'invalid_arguments' == 'local_service_unavailable')f6a3dc7→5427211Refuse unusable hello values at startup and status-less receiptshelloanswer whose module names are not strings made every later call raiseTypeError, and an Orrery receipt withoutstatusmade the waiting loops raiseKeyError. The launcher now refuses such an answer (unsupported_protocol_version), andanswered_receipttreats a receipt without a stringstatusas no receipt.test_launcher.py,test_client_errors.py: 4 failed, 26 passed26f9f04Say how the interface and emitter versions relateRe-run after the last commit:
tests/interface,tests/test_boundaries.py,tests/test_package_names.pyandtests/test_workflows.py, 126 passed.ruff check,ruff format --checkandtools/check-changelog.shpass.Unchanged, as open decisions:
AGENTS.md's publishing rule.🤖 Generated with Claude Code