Read-the-room admission gate for agents on shared chat surfaces: should this agent speak now?
Project description
Nunchi
nunchi (눈치, NOON-chee): the art of reading the room and knowing whether it is your turn to speak. This library gives your agent that.
Nunchi is a portable CLI/library for deciding whether an agent should visibly participate on an unstructured shared surface before ordinary reply generation. It returns an auditable admission verdict:
PASS— hard stop; no ordinary visible replyACK— brief acknowledgement is warrantedASK— clarification is warrantedSPEAK— substantive contribution is warranted
Status
The current classifier slice exposes a product/default admission classifier path
backed by a configured provider/model. Successful results include the selected
classifier identity, provider/model audit fields, verdict, confidence
distribution, checked context, and reasons. There is no public deterministic
classifier path; offline/CI evidence uses a test fixture provider behind the
product path.
The first adapter ships alongside the core: nunchi.adapters.channel
maps a channel-local message shape to an admission request and routes the
verdict for a participant agent (see "Consuming the gate" below). Live
Discord/cc-connect process integration, central orchestration, broad benchmarks,
launch claims, and reply composition remain out of scope — the adapter produces
the sentinel an existing cc-connect deployment already understands; wiring it
into a running bot is the consumer's step.
Install
Stdlib-only (Python 3.11+, no runtime dependencies). Not yet on PyPI; install from source:
pip install "git+https://github.com/mentatzoe/nunchi.git" # or: pip install .
# zero-install one-shot:
uvx --from "git+https://github.com/mentatzoe/nunchi.git" nunchi --help
This provides the nunchi and nunchi-channel console scripts. See
CHANGELOG.md for releases and docs/STABILITY.md
for the versioning / verdict-surface stability contract.
Quickstart
Evaluate a request from stdin through the product/default classifier:
export NUNCHI_CLASSIFIER_MODEL="your/provider-model"
export OPENROUTER_API_KEY="..."
PYTHONPATH=src python3 -m nunchi admit < tests/fixtures/speak.json
Evaluate a request from a file through the product classifier:
PYTHONPATH=src python3 -m nunchi admit --input tests/fixtures/pass.json
Evaluate with classifier selection in the envelope, or override it from the CLI:
PYTHONPATH=src python3 -m nunchi admit --input tests/fixtures/speak_with_classifier.json
PYTHONPATH=src python3 -m nunchi admit --classifier product --input tests/fixtures/speak_cli_precedence.json
Run the verification suite:
python3 -m unittest
Product contract
The core output contract is:
classifierclassifier_providerclassifier_modelverdictconfidencescontext_checkedreasons
Successful CLI evaluations write one JSON object to stdout and exit 0.
Failures write diagnostics to stderr and do not emit a success verdict on
stdout.
Exit codes:
0— successful evaluation1— unexpected runtime failure2— input source or JSON parse failure3— admission request validation failure
Nunchi owns admission, not composition. It does not draft the final reply and it does not prescribe speech shape beyond the admission verdict.
Classifier selection
The documented default classifier path is product. It is the only supported
classifier path in this slice and is backed by a configured OpenAI-compatible
provider/model. It is not a relabelled local keyword or deterministic verifier.
If provider/model configuration is unavailable, Nunchi fails clearly instead
of silently falling back to local logic.
Classifier selection can be supplied by:
- envelope field:
"classifier": "product" - CLI flag:
--classifier product
If both are present, the CLI flag takes precedence. Optional
classifier_config / --classifier-config must be a JSON object. Supported
product configuration keys are provider, model, and timeout. Unsupported
classifier names or config keys fail clearly without emitting a success result.
The provider endpoint and API key are operator-only and are never read from
classifier_config: because a request envelope carries classifier_config, an
untrusted request must not be able to redirect the provider call (which carries
the operator's API key) or choose which environment variable the key is read
from. These are resolved exclusively from operator environment variables:
NUNCHI_CLASSIFIER_MODELfor the model name (orclassifier_config.model).NUNCHI_CLASSIFIER_API_KEYorOPENROUTER_API_KEYfor the API key.NUNCHI_CLASSIFIER_BASE_URLorOPENAI_BASE_URLfor the compatible API base URL; default ishttps://openrouter.ai/api/v1.
The test suite sets a fixture provider response for deterministic offline verification. That fixture provider is not a selectable classifier path.
Python API
The in-process core is available without shelling out:
import os
import sys
sys.path.insert(0, os.path.abspath("src"))
from nunchi import evaluate
os.environ["NUNCHI_CLASSIFIER_MODEL"] = "your/provider-model"
os.environ["OPENROUTER_API_KEY"] = "..."
result = evaluate({
"trigger": {"content": "nunchi-vigil, please implement the CLI MVP."},
"context": [],
})
result["classifier"] identifies the selected path, result["classifier_model"]
identifies the provider model, and result["verdict"] is one of PASS, ACK,
ASK, or SPEAK.
Consuming the gate: the channel adapter
A participant agent on a shared, turn-aware surface does not call the core
directly — it uses the channel adapter (nunchi.adapters.channel), which
maps its channel-local inputs (the triggering message, the recent transcript,
its own identity) to an admission request, runs the gate, and returns a
transport-neutral decision: verdict plus silent. If silent, the host posts
nothing; otherwise it composes one turn in the returned run-shape. The adapter
never writes replies, and nothing in it is tied to a specific chat platform.
In-process (Python host):
from nunchi.adapters.channel import gate
result = gate(
{"content": "dalgos, summarize the cache tradeoffs", "author": "zoe",
"author_kind": "human", "message_id": "m-42"},
history=[ # last ~10 channel messages, oldest first
{"content": "I'd go in-process LRU.", "author": "vigil",
"author_kind": "peer_bot", "message_id": "m-41"},
],
agent_id="dalgos", # plus optional agent_role / agent_mention_id
pinned_rules=None, # optional channel governance text
fail_policy="open", # open->SPEAK | closed->PASS | raise
)
if result.silent:
... # post nothing this turn
else:
... # compose a turn per result.verdict / result.run_shape
Subprocess (non-Python host) — JSON in, a transport-neutral JSON directive out:
echo '{"trigger":{"content":"vigil, rebase the branch","message_id":"m-1"},
"history":[],"agent":{"id":"dalgos"},"fail_policy":"open"}' \
| PYTHONPATH=src python3 -m nunchi.adapters
# -> {"verdict":"PASS","silent":true,...} (host posts nothing)
# -> {"verdict":"SPEAK","silent":false,...} (host composes a turn)
If your transport suppresses a send via a magic final-output string, pass your
own with --silent-token "<token>" (or result.silent_token("<token>")) to
print it on PASS. cc-connect is just a named preset of this — --format cc-connect ≡ --silent-token CC_CONNECT_SILENT_PASS — with no special status;
no transport is a dependency.
Room governance profiles
The classifier core judges by plain social sense: who is speaking, what has
been said, who this agent is — is it this agent's turn? It carries no room
doctrine of its own. A room that wants a specific bar for taking a turn
supplies its norms as pinned_rules; the classifier applies the room's bar
with precedence over plain social sense.
profiles/open-floor.md ships as the first reusable
profile: the strict operator-led working-channel doctrine of the original
open-floor pilot (default PASS, net-new-value bar for SPEAK, rare ACK,
operator-only directives, corroboration for completion claims). Pass its text
as pinned_rules to opt a channel into that regime:
from pathlib import Path
from nunchi.adapters.channel import gate
result = gate(trigger, history=history, agent_id="dalgos",
pinned_rules=Path("profiles/open-floor.md").read_text())
Verdict-suite fixtures whose expected verdicts were adjudicated under that
doctrine declare "governance_profile": "open-floor" in their metadata; the
suite loader injects the profile into their envelopes so the corpus stays
honest about which expectations are social sense and which are room policy.
docs/integration.md is the full integration guide —
scope, the three install/integration paths (loader instruction, in-process
import, subprocess CLI), how to wire it into a channel adapter, and how to
generalize to other surfaces. A runnable multi-turn demo is in
examples/read_the_room_demo.py; the adapter
contract is in specs/004-read-the-room-adapter/spec.md.
This is the adapter tier (Constitution VI): it depends on the core and is not a
live Discord integration — it produces the sentinel an existing cc-connect
deployment already understands.
Verdict test suite
The classifier verdict test suite is the merge contract for classifier
changes: a fixture corpus of observed and predicted failure modes
(Multica-shaped agent traffic, Discord-shaped human conversation, and
verdict-surface contract cases) run through a pluggable adapter against any
classifier candidate. The single entry command is
python3 specs/003-classifier-test-suite/contracts/runner.py; see
specs/003-classifier-test-suite/quickstart.md
for the offline deterministic path, live evidence runs, and how to add a
fixture.
Development method
This repository uses Spec Kit. The constitution at
.specify/memory/constitution.md is the source of governance for all specs,
plans, tasks, implementation, documentation, and release claims.
For production work, use:
constitution -> specify -> clarify -> plan -> checklist -> tasks -> analyze -> implement
A product spec should prove an end-to-end runnable path from supplied conversation context to a verdict a harness can obey.
License
Nunchi is dual-licensed under MIT OR Apache-2.0, at your option. See
LICENSE-MIT and LICENSE-APACHE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nunchi-0.2.0.tar.gz.
File metadata
- Download URL: nunchi-0.2.0.tar.gz
- Upload date:
- Size: 50.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2282bd985d0b4b7ee355b0d7a5cdf0161318c887d968d5548ca10c85a7c227f0
|
|
| MD5 |
55e32e43ebb3a96a585d231bd14f70e4
|
|
| BLAKE2b-256 |
29f11b458c7d8dac6f33490aadd025f2cf986393b035cd906db2c40c717c6e0a
|
Provenance
The following attestation bundles were made for nunchi-0.2.0.tar.gz:
Publisher:
release.yml on mentatzoe/nunchi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nunchi-0.2.0.tar.gz -
Subject digest:
2282bd985d0b4b7ee355b0d7a5cdf0161318c887d968d5548ca10c85a7c227f0 - Sigstore transparency entry: 2047479346
- Sigstore integration time:
-
Permalink:
mentatzoe/nunchi@f925633e3badc14c499ede37f7fecd6f11418354 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/mentatzoe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f925633e3badc14c499ede37f7fecd6f11418354 -
Trigger Event:
push
-
Statement type:
File details
Details for the file nunchi-0.2.0-py3-none-any.whl.
File metadata
- Download URL: nunchi-0.2.0-py3-none-any.whl
- Upload date:
- Size: 32.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7d4a84537331874961196924cc39fc8970a53b6a527153da91299206b63908d
|
|
| MD5 |
7f00ddea8cdee1068ab9416e017d84bf
|
|
| BLAKE2b-256 |
46a44f3172f8bc962d76571e12a345471d461ec28bc7aade6df726f3242057c1
|
Provenance
The following attestation bundles were made for nunchi-0.2.0-py3-none-any.whl:
Publisher:
release.yml on mentatzoe/nunchi
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nunchi-0.2.0-py3-none-any.whl -
Subject digest:
f7d4a84537331874961196924cc39fc8970a53b6a527153da91299206b63908d - Sigstore transparency entry: 2047479373
- Sigstore integration time:
-
Permalink:
mentatzoe/nunchi@f925633e3badc14c499ede37f7fecd6f11418354 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/mentatzoe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f925633e3badc14c499ede37f7fecd6f11418354 -
Trigger Event:
push
-
Statement type: