AGENTS.md@backend/packages/harness/deerflow/typesafe · git:20260927.d700ad9 · 2026-09-27 · sha256 35c69939432691e2
AGENTS.md@backend/packages/harness/deerflow/typesafe git:20260927.d700ad9A
Immutable. This exact content never changes and is served at /api/v1/blob/35c69939432691e2.
### Shared TypeSafe client `typesafe/` is the transport layer for every TypeSafe (Jev) call site: the guardrail risk gate (`guardrails/typesafe.py`), and — once enabled — memory pre-screening and signal classification. It owns exactly transport and lifecycle, authentication, retry and backoff, the deadline budget, response parsing and the error taxonomy, UTF-8 wire-size counting, and the request skeleton. Clients live for one evaluation: no cache, no connection pool, and `transport_factory` is always a factory because httpx closes the transport it was handed. **It must not gain, and no consumer may move into it:** state content, the questions/criteria/thresholds and which way they decide, the failure policy (the gate denies on error, the memory paths carry on), cache semantics, or business audit. Two consumers with different thresholds are expected to share one request, so policy parameters can never reach this layer — they belong to each consumer's `release_policy_parameters()`, while `sharing_key()` carries only the credential fingerprint, the connection settings, the consumer's input limit and the transport identity. Keeping that split is the whole point: contaminating either identity either merges requests that must stay apart or breaks assembly fingerprinting. Two-layer responses are load-bearing. A request-level failure (transport, deadline, non-200, missing envelope, unparsable-or-unbounded body) raises `TypeSafeError` with a `cause`, and the consumer decides deny / fall back. A question-level failure is **data** (`AnswerSet.errors_by_question`), never an exception, so one bad answer cannot discard the other valid answers in the same response; the tool gate maps its own question's error back to `TypeSafeGuardrailError` because it has no verdict without it. The reply is never read unbounded and never decoded: the client asks for an identity encoding, refuses a body that carries a non-identity `Content-Encoding`, and stops past `MAX_RESPONSE_BYTES` (64 KiB, a few hundred bytes is the expected envelope), so an endpoint that answers with something other than an answer set cannot be expanded into memory or accepted as a verdict. Deep nesting is mapped too — `json.loads` raises `RecursionError`, not `ValueError`, and it must not escape this taxonomy. The deadline covers the read, not only the headers: the sync path, which cannot preempt the read it is blocked in, re-checks the budget before each transport read, so a body that drips just under the read timeout cannot hold the caller for the whole of `MAX_RESPONSE_BYTES` past `deadline_seconds`. Limits and secrets must not move. `max_state_chars` stays a character count in every consumer — `wire_size` reports bytes and replaces no limit, and one CJK character is three of them. A credential never reaches a log, an error message, a policy identity, a reason message or `repr()` (`TypeSafeConnection` hides both the key and its environment-variable name); only `sha256` fingerprints are compared. Two rules keep that promise true at the edges: a credential that could not travel as a header value (surrounding whitespace, a non-printable character) is refused when the connection resolves, and a transport failure is raised without chaining the original exception, whose message can contain the request it refused. Credential resolution is per *layer* too — the first layer that sets `api_key` or `api_key_env` decides it, and inside one layer the literal key wins — so a consumer naming an environment variable is never silently switched to another layer's key. Likewise `base_url` must be an `http(s)` URL with no query, fragment or embedded credentials, checked at construction rather than on every request.