8.8 KiB
Domain ① — Capabilities (CI pivot revamp · WIP)
WIP design doc. Part of the Phase 4 → end revamp (the old "pipeline"). Scope guardrail: Phases 1–3 are SHIPPED and FIXED — the rename, and the proprietary crawler/proxy/billing/stealth/captcha stack. This domain builds on them and never modifies them.
Role in the universe
FIXED foundation (done): Acquisition (crawler moat) · Metering (03c) · Identity/Tenancy
OUR scope (this revamp): ▶ Capabilities ◀ → Access → Intelligence + Timeline → Triggers
Capabilities sits directly on the fixed foundation and is consumed by Access (the doors) and the Intelligence hot loop. It is the first domain because everything above it calls into it.
Purpose
Turn the fixed Acquisition engine (+ a new Google Maps actor) into a small set of typed, callable verbs that every door (chat / REST / MCP) and the Intelligence loop consume identically.
This is the core pivot of old Phase 4: replace "connector that ingests into the KB" with
"capability that returns data." A capability is a stateless function you call and get data
back from — no SearchSourceConnector row to configure, no KB write, no schedule attached.
Foundation it builds on (do not touch)
WebCrawlerConnector.crawl_url(url) -> CrawlOutcome— single-URL fetch;{content, metadata, crawler_type}onSUCCESS; billable predicate single-sourced asstatus == SUCCESS. (app/proprietary/web_crawler/.)WebCrawlCreditService(03c) — per-success metering + thebill=seam.- Proxy / stealth / captcha tiers — all behind
crawl_url; callers never see the tier.
The verb set (MVP) — namespaced, nothing top-level
Two namespaces: web.* (generic crawler product) and maps.* (the one platform actor).
Future platforms slot in as their own namespace (linkedin.*, amazon.*, …).
| Verb | Input → Output | Mode | Executes over | Bills (03c) |
|---|---|---|---|---|
web.scrape(urls[]) |
→ [{url, status, content, metadata}] |
inline-or-job | loop crawl_url |
per success |
web.discover(query, top_k) |
→ [{url, title, snippet, provider}] |
inline | search providers (SearXNG/Linkup/Baidu) | per search (or free — open) |
maps.search(query, location) |
→ [place] |
job | Maps actor (new, proprietary) | per place |
maps.place(place_id|url) |
→ place (structured) |
inline | Maps actor (new, proprietary) | 1 |
maps.reviews(place) |
→ [review] (paged) |
job | Maps actor (new, proprietary) | per page |
web.scrape — one array-friendly verb (no separate "batch")
Scraping always welcomes an array of URLs. There is no separate scrape_batch. One verb,
one mental model: "give me URLs, I give you per-URL content."
Execution mode — a property of the result, not two verbs
A capability does not split into sync/async variants. Every call returns a uniform result envelope:
{ status: "completed" | "pending", job_id?: str, progress?: {done, total}, results?: [...] , error?: ... }
- inline (fast path): small/fast input → finishes in-request → envelope returns
completedwithresultsinline → zero polling. (single page, one place, a search) - job (slow path): large/slow input → envelope returns
pending+job_id→ caller polls a status endpoint untilcompleted, then readsresults. (batch scrape, Maps search/reviews)
So "sync" is simply a job that finished instantly. Same verb, same response shape, no magic branching for the caller to reason about.
- Threshold (how many URLs / how heavy before it goes async) is configurable.
- A caller may pass
async: trueto force a job even for small input (agents that never want to block). - Only infra this requires: a thin job record (
id, status, progress, result_ref) + the existing Celery workers. Not a resurrectedpipeline_runs. Sync verbs need none of it.
The capability registry — single source of truth
One registry entry per verb:
Capability {
name # dotted, e.g. "web.scrape", "maps.search"
input_schema # Pydantic
output_schema # Pydantic
mode # can-complete-inline? + job-capable?
executor # the async fn (wraps Acquisition / Maps actor)
billing_unit # how Metering charges this call
}
The three doors are generated from the registry, not hand-written three times:
- chat tool ← entry (tool def + handler)
- REST route ← entry (path + request/response models)
- MCP tool ← entry (MCP tool schema + handler)
…and the Intelligence hot loop calls the same executor directly. Add a verb once → it
lights up on every surface; the I/O contract cannot drift between surfaces.
Where it lives / license boundary
- New Apache-2 package
app/capabilities/(registry, schemas, executors, the thin job store). - It imports from the proprietary Acquisition engine but never moves into it.
- The Maps extractor logic is proprietary (
app/proprietary/...), consumed by themaps.*executors — same boundary rule Phase 3 set (Apache code may import from proprietary, not move into it).
Billing integration — open to the billing service (not hardcoded per verb)
Billing is not baked into each verb. A capability only declares a billing_unit in its
registry entry; charging is delegated to the billing service, so adding/repricing a unit is a
billing-service concern, not a capability rewrite. 03c's WebCrawlCreditService is the first
provider; new units register with the same service.
web.scrape→ perSUCCESS(existingweb_crawlunit).maps.*→ register a new per-place / per-page unit with the billing service (same wallet).- captcha attempts → existing per-attempt
web_crawl_captchaunit (unchanged). web.discover→ register a per-search unit (or mark free) with the billing service.
The point: the registry says "this verb bills unit X"; the billing service owns what unit X costs and how it settles. Verbs stay pure; pricing stays pluggable.
The Maps actor (clarification)
- Google-Maps-agnostic — works for any place type (restaurants, gyms, hotels, retail, …).
- Returns typed structured objects (
{name, rating, review_count, hours, price_level, …}), not raw markdown — which is exactly what the Intelligence/Timeline layer needs to diff reliably. - The "restaurant" use case is an Intelligence-domain wedge (one Tracker), not a constraint on the capability. The capability never knows what decision it serves.
Relationship to the drafted Phase 4
- 04b (source-discovery) → absorbed as
web.discover+ relocating Linkup/Baidu keys from per-connectorconfigto platform env (SearXNG already env-based). Tavily/Serper dropped. - 04a (connector taxonomy + MCP routing) → demoted to backward-compat hygiene. The
capability registry is the new spine, so the Type-1/Type-2 connector registry is no longer
central. 04a's "turn off branded connectors" stays a small, separable cleanup for the legacy
chat agent; the BYO-
MCP_CONNECTORrouting fix belongs to the Access/Conversation domain (it lets the agent use the user's external tools — a different concept from our capabilities).
Deferred (not in MVP)
- Recursive site crawl (follow links across a whole site) — doesn't exist today; the
"crawl a topic" story is
web.discover→web.scrape. - A generic
web.extract(LLM-structured extraction as a standalone verb) — structured extraction lives in the Intelligence loop for MVP; promote to a capability later if devs want it. - Additional platform namespaces (
linkedin.*,amazon.*, …).
Locked decisions
- Verbs:
web.scrape·web.discover·maps.search·maps.place·maps.reviews. Namespaced; nothing top-level. - One array-friendly
web.scrape(no separate batch verb). - Execution mode = result property (uniform
completed/pendingenvelope), not separate verbs. - Build the thin job model now (Maps search / batch scrape force it).
- One capability registry → generates chat/REST/MCP doors + feeds the Intelligence executor.
app/capabilities/Apache-2; Maps extractor proprietary; billing delegated to the billing service (verbs declare abilling_unit;03cis the first provider, new units register there).- Connector → capability is a replacement for these data sources, not an extension.
Open questions (carry forward)
- Default async threshold for
web.scrape. - The Google Maps actor is net-new (not part of shipped Phases 1–3) — designed/built as a
separate effort (incl. sourcing legality).
maps.*verbs are contracts against it; this domain doesn't block on it.