Compile any content into a tiered, budget-aware, agent-navigable progressive-disclosure manifest, plus an on-demand expansion protocol — for small/on-device context windows
Project description
askfaro-progressive-context
Compile any content into a tiered, budget-aware, agent-navigable progressive-disclosure manifest — for small / on-device context windows.
On-device models have tiny context windows (~4k today, ~32k near-term). Stuffing everything in — or lossily compressing it — loses information. The alternative is progressive disclosure: give the model a compact, accurate index of what exists and when each piece is relevant, then let it fetch detail on demand within a hard token budget.
askfaro-progressive-context is the open-source compiler + format + runtime for that index. It is the agent-navigated half of Faro's context tooling (the model reads the index and decides what to expand); its sibling askfaro-embedded-search is the retrieval-driven half. The two stay independent — this library has no hard dependency on it.
Status: pre-alpha, but published —
pip install askfaro-progressive-context(v0.4.0 on PyPI). Shipped and tested: the format, the compiler (pcx build) with adapters fortools/docs/skills/memory, the descriptor engine (bottom-up + contrastive + self-grade), incremental rebuilds (descriptor reuse keyed oncontent_hash), the expansion runtime, theNavSessionagent-loop API (index/look/open/close,local/remotemodes), identity-revalidated manifest caching (ManifestLoader/AsyncManifestLoader), and the eval harness. Still to come: per-budget frontier shaping,website/fileadapters with clustering, and the hosted Faro registry.
The idea in one screen
A progressive-context manifest (pcx.json) is a tree of nodes. Every node carries:
- a descriptor —
what(one line: what this is) andwhen(one line: when it's relevant). These are the navigation index, and their quality is the whole game. - token costs — so the runtime can plan expansion against a budget without fetching anything.
- either children (a branch) or a payload pointer (a leaf). Leaves are never inlined and always verbatim — no information is lost to a summary.
- optionally (v0.2)
links— see-also cross-links{to, why}to related nodes in other branches, the lateral navigation a pure tree can't express — andfacets— orthogonal tags (type, status, …) you filter on to narrow before reading descriptors.
Variants are pre-generated per budget (pcx.4k.json, pcx.32k.json, …); budgets are arbitrary integers, so a developer who needs headroom for their own content can build a 31k variant — or reserve it at runtime.
What's here (v0.4.0)
| Module | What it does |
|---|---|
schema/pcx-0.2.schema.json |
the format, as JSON Schema (v0.2: adds cross-links + facets) |
askfaro_progressive_context.types |
Manifest / Node / Link / Payload / Variant dataclasses |
askfaro_progressive_context.validate |
structural (zero-dep) + JSON Schema validation |
askfaro_progressive_context.build (pcx build) |
the compiler: adapters, descriptor engine (bottom-up + contrastive convergence + dual-channel self-grade + optional branch synthesis), distinctiveness/fidelity metrics + CI gates, cross-link inference, tree-shape lints, incremental rebuilds, per-budget emit + llms.txt |
askfaro_progressive_context.runtime |
the expansion protocol: peek / expand / collapse / search / related / find_by_facets / reconcile, with hard budget enforcement and a runtime reserve |
askfaro_progressive_context.session |
NavSession — the agent-loop API (index/look/open/close/related/filter) with local/remote modes + leaf context envelopes |
askfaro_progressive_context.navigator |
KeywordNavigator (deterministic baseline, no model) and LLMNavigator (bring your own complete()) |
askfaro_progressive_context.loader |
ManifestLoader / AsyncManifestLoader + MemoryStore / FileStore — transport-agnostic, identity-revalidated manifest caching |
askfaro_progressive_context.eval |
the navigation-success @ budget harness — the headline quality metric |
Quick start
pip install askfaro-progressive-context # runtime + CLI
pip install "askfaro-progressive-context[llm]" # + the LLM descriptor engine for `pcx build`
# build manifest variants from a source tree (use --fake for an offline/CI build)
pcx build ./skills --kind skills --budgets 4k,32k --endpoint $ENDPOINT --model $MODEL
# validate a manifest
pcx validate examples/skills/manifest.pcx.4k.json --schema
# score navigation-success @ budget with the deterministic baseline navigator
pcx eval examples/skills/manifest.pcx.4k.json examples/skills/cases.json -v
Working from a clone? Install editable with the dev + schema extras instead:
pip install -e ".[dev,schema]".
from askfaro_progressive_context import Manifest, Runtime
m = Manifest.from_dict(json.load(open("examples/skills/manifest.pcx.4k.json")))
rt = Runtime(m, reserve=1024) # leave 1k for your own content
rt.peek() # frontier: tier-1 descriptors + budget_remaining
rt.expand("recurring") # reveal a branch's children (charged against budget)
ref = rt.expand("recurring.create") # splice a leaf's verbatim payload; raises if over budget
The expansion protocol
The runtime — not the model — is the budget authority. effective_budget = variant.budget − reserve, and every expand is checked against it. When full it auto-collapses LRU leaves (opt-in) or refuses and tells the agent to choose. The budget is never silently exceeded.
Driving it from an agent loop: NavSession
Runtime is the low-level budget authority; NavSession wraps it with the three verbs an agent loop actually drives, plus mode-aware defaults. The model's choice of verb is the confidence signal — there is no threshold to tune:
from askfaro_progressive_context import Manifest, NavSession
s = NavSession(manifest, mode="local", reserve=1024)
s.index() # current frontier, shortest-useful view first
s.look(["recurring", "one_off"]) # escalate candidates to full descriptors WITHOUT opening them
s.open("recurring") # branch -> drill into its children;
# leaf -> splice the verbatim content (budget-enforced)
s.close("recurring") # collapse a node to reclaim budget
s.shown_tokens # everything the model has seen this session (the real "length")
s.budget_remaining
If the index is enough, the model calls open; if it can't decide, it calls look first. Modes encode the tokens-vs-round-trips tradeoff:
| mode | frontier view | small leaves | use when |
|---|---|---|---|
local (default) |
brief |
resolved on demand (O(1) resident splice) | on-device / resident manifest — round-trips are ~free, so take many tiny steps |
remote |
full |
inlined into index() (≤200 tokens) |
network-backed — each hop costs latency, so disclose more per step to need fewer |
Pass config=ModeConfig(...) for a custom policy; an unknown mode raises with the valid options.
Beyond the tree: cross-links, facets, freshness (v0.2)
A pure tree gives you global (where am I) and local (this neighborhood) navigation, but not the contextual "this also relates to that over in another branch." pcx v0.2 adds three things for that, all optional and additive:
s = NavSession(manifest, mode="local")
s.related("posts.schedule") # explore: see-also links (with why-phrases)
# to related nodes in OTHER branches
s.filter(kind="template") # facet-first precision: ids matching every
# facet key=value, before you read descriptors
- Cross-links (
node.links,Runtime.related) are the lateral edges. Build them withpcx build --cross-links, which infers see-also links from cross-branch descriptor similarity and stamps a why-phrase;build.links.betweennessthen flags the bridge nodes those links create (the descriptors most worth getting right). - Facets (
node.facets,Runtime.find_by_facets) are orthogonal dimensions. Filtering on two independent 5-value facets cuts the space ~25× before any descriptor ranking — multiplicative precision for cheap. - Reconciliation (
Runtime.reconcile) treats a stale manifest as the hazard it is: an agent trusts a curated map completely, so a stale one misroutes silently. Pass the origin's current identity and/or the set of live ids and it returns staleness / dangling-route warnings instead of serving them blind.
warnings = rt.reconcile(current_identity=origin_hash, live_ids=still_present)
if warnings: # refetch before trusting the routes
...
Caching the manifest: ManifestLoader
A manifest is small, identical for every reader, and changes only when its source content changes — an ideal cache target. But it is also a routing index, so a stale copy can point an agent at content that has moved or vanished. A plain time-to-live cache is therefore the wrong tool: the safe pattern is to cache on the manifest's identity and revalidate against it, never to expire blindly on a clock.
ManifestLoader gives every consumer that pattern by default, while staying transport-agnostic — it knows nothing about HTTP, files, S3, or ETags. You supply a Fetcher that reaches your origin; the loader owns the identity bookkeeping, the store, and the guarantee that a changed identity always wins.
from askfaro_progressive_context import (
FetchOutcome, FileStore, ManifestKey, ManifestLoader, identity_of,
)
import httpx
def fetch(key: ManifestKey, known_identity: str | None) -> FetchOutcome:
# Your transport decides "changed?" however it likes. Over HTTP, let the
# origin do it with a conditional request:
headers = {"If-None-Match": known_identity} if known_identity else {}
r = httpx.get(f"https://api.example.com/pcx/manifest?budget={key.budget}", headers=headers)
if r.status_code == 304:
return FetchOutcome.unchanged() # reuse the cache, no transfer
body = r.json()
return FetchOutcome.fresh(r.headers.get("ETag") or identity_of(body), body)
loader = ManifestLoader(fetch=fetch, store=FileStore("~/.cache/pcx"))
manifest = loader.load(ManifestKey(source_id="my-catalog", budget="4k")) # -> Manifest
Your fetch(key, known_identity) is handed the identity already in cache (or None) and returns one of two outcomes:
FetchOutcome.unchanged()— nothing changed sinceknown_identity(e.g. a 304). The loader reuses the cached body: zero transfer, zero parsing.FetchOutcome.fresh(identity, body)— fresh content. The loader stores it under the key and returns it.
The safety property is structural: a Fetcher too dumb to do conditional requests and always returning full content still gets correct invalidation, because the store is identity-stamped and re-storing the same identity is a no-op. There is no TTL knob that can serve a stale routing index past an identity change.
| piece | role |
|---|---|
ManifestKey(source_id, budget) |
what you want — budget is part of the key (identity is shared across budgets) |
identity (a str) |
opaque content token: a content hash, an ETag, "v{n}" — compared for equality only, never parsed |
Fetcher |
your transport: (key, known_identity) -> FetchOutcome. The one bit you write |
MemoryStore (default) |
process-local; all a long-running process needs |
FileStore(dir) |
on-disk; earns its keep for short-lived processes (CLIs, serverless, edge cold starts) that would otherwise re-download every run |
identity_of(dict) |
derive an identity from a manifest body (prefers source.content_hash, else hashes the body) — for transports with no native validator |
Identity comes from the build: emit stamps each manifest with a bottom-up source.content_hash, surfaced as manifest.identity. load() means "give me the current manifest, revalidating." If you want to skip even the revalidation round-trip, don't call load() in a hot loop — hold the returned Manifest and reload on your own cadence.
For an async transport (e.g. an httpx.AsyncClient), use AsyncManifestLoader — same contract, but fetch is a coroutine and load() is awaited. The store stays synchronous (its get/put are fast local operations):
from askfaro_progressive_context import AsyncManifestLoader, FetchOutcome, FileStore, ManifestKey
async def fetch(key, known_identity):
r = await async_http.get(f"/pcx/manifest?budget={key.budget}",
headers={"If-None-Match": known_identity} if known_identity else {})
if r.status_code == 304:
return FetchOutcome.unchanged()
return FetchOutcome.fresh(r.headers.get("ETag") or identity_of(r.json()), r.json())
loader = AsyncManifestLoader(fetch=fetch, store=FileStore("~/.cache/pcx"))
manifest = await loader.load(ManifestKey(source_id="my-catalog", budget="4k"))
For origins: to let clients revalidate cheaply, serve the manifest with an
ETag(thesource.content_hashis a ready-made one) and honorIf-None-Matchwith a304. Without that, clients still cache correctly viaidentity_of, they just re-transfer the body on eachload().
Why a benchmark, not vibes
The moat is descriptor quality, so quality is measured, not asserted. The eval harness gives a navigator only the manifest and a budget plus (query → correct leaf) cases, and reports navigation-success @ budget, first-hop precision, and average hops. The deterministic KeywordNavigator establishes an offline floor; swap in an LLMNavigator to score a real model.
Enforced at build time, not just measured after
The eval scores a finished manifest; the compiler also gates descriptor quality as it builds, so regressions fail fast:
- Distinctiveness. The contrastive pass is a convergence loop over a lexical similarity measure: it rewrites colliding siblings until the worst pair drops below
--collision-threshold, and — when a level is wider than one contrast call — groups siblings by similarity so near-duplicates are actually compared.--max-collisionfails the build if any siblings stay too alike. - Fidelity (predict-then-verify).
--fidelityscores whether each descriptor lets you predict its node's content (lexicaloffline,llmfor real);--min-fidelitygates it. This catches descriptors that read well but don't anticipate the content. - Vacuity/retrieval channel. A descriptor that just restates the title, or whose
whenis all connective filler, reads fine but is un-searchable — a deterministic check forces a repair, and the LLM grade scores navigation and retrieval channels. - Branch synthesis.
--synthesiswrites branch descriptors from descendant content (what unifies the group, tensions, where to start) instead of assembling child descriptors. - Shape lints. Over-wide levels, over-deep/small-corpus trees, and does-too-much branch
whatsurface as warnings;--flattencollapses single-child hops;--presetfills a coherent config with a justification per setting.
Length is the point
Accuracy without length misses why this exists. The eval reports
first_view_tokens and tokens_to_answer next to accuracy, and the runtime
renders the frontier at progressively shorter levels (title → brief →
full) so the agent opens with the shortest view and escalates only when
unsure. On a 24-tool catalog, a title-first view is ~14× smaller than the full
descriptor set, and the model reaches the right tool having seen ~6.9× less
context than loading every schema.
Progressive disclosure trades tokens for round-trips, so the whole artifact is
meant to stay resident: Runtime(resolver=...) resolves leaves from a local
store, making every expand an O(1) splice rather than a network fetch.
Troubleshooting
Setup mistakes fail with actionable messages; see docs/troubleshooting.md
for the full table (missing extras, model config, architecture mismatches).
License
MIT © Faro
Project details
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 askfaro_progressive_context-0.4.0.tar.gz.
File metadata
- Download URL: askfaro_progressive_context-0.4.0.tar.gz
- Upload date:
- Size: 76.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0e74552f9e0c74eb92897ad1408247238fcbef42c4e993ecc40870022f871c17
|
|
| MD5 |
c6c287da153928f38d24eaae8153f5ac
|
|
| BLAKE2b-256 |
acf19483fe5bb0eb7aae68b2a20139c8ee4ef775b226541cb617da63c38b018f
|
Provenance
The following attestation bundles were made for askfaro_progressive_context-0.4.0.tar.gz:
Publisher:
publish.yml on poolside-ventures/askfaro-progressive-context
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
askfaro_progressive_context-0.4.0.tar.gz -
Subject digest:
0e74552f9e0c74eb92897ad1408247238fcbef42c4e993ecc40870022f871c17 - Sigstore transparency entry: 2099559853
- Sigstore integration time:
-
Permalink:
poolside-ventures/askfaro-progressive-context@28e684b16384ef2aaaf4414eb83b8cb57fd948da -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/poolside-ventures
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@28e684b16384ef2aaaf4414eb83b8cb57fd948da -
Trigger Event:
release
-
Statement type:
File details
Details for the file askfaro_progressive_context-0.4.0-py3-none-any.whl.
File metadata
- Download URL: askfaro_progressive_context-0.4.0-py3-none-any.whl
- Upload date:
- Size: 69.5 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 |
48707e303426342ce748a8908b25cce326705c45d7991219207bd9a533b547ad
|
|
| MD5 |
fbaa29360390099fa0631e6da6afccee
|
|
| BLAKE2b-256 |
b436d46bb65db47a3f047263b8e78e2c63c5034442ff9d77a55da22f494bd576
|
Provenance
The following attestation bundles were made for askfaro_progressive_context-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on poolside-ventures/askfaro-progressive-context
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
askfaro_progressive_context-0.4.0-py3-none-any.whl -
Subject digest:
48707e303426342ce748a8908b25cce326705c45d7991219207bd9a533b547ad - Sigstore transparency entry: 2099560293
- Sigstore integration time:
-
Permalink:
poolside-ventures/askfaro-progressive-context@28e684b16384ef2aaaf4414eb83b8cb57fd948da -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/poolside-ventures
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@28e684b16384ef2aaaf4414eb83b8cb57fd948da -
Trigger Event:
release
-
Statement type: