Documentation
¶
Overview ¶
Package mcpserver exposes the daemon's research surface over MCP (§5.1).
Separate from internal/daemon so that package stays transport-only: the socket and its permissions are one concern, what a caller can ask for is another, and this one is testable over an in-memory transport with no socket at all.
The tools are asynchronous by design (§5.3). research.report returns a session id and nothing else; the work continues in the daemon long after the calling agent's context is gone, and the caller polls research.status the way it would wait on a CI job.
Index ¶
Constants ¶
const ( MaxSourcesCeiling = 25 MaxDepthCeiling = 5 )
Ceilings on what a caller may ask for per lead and per session.
Generous — a legitimate agent will never reach them — and finite, which is the point: every value here is multiplied into a resource bound.
const AllClaims = 100_000
AllClaims is the limit to pass when every claim of a session is wanted.
store.ListClaims treats a non-positive limit as 500, which is a sensible default for a display and a wrong one here: with 600 claims recorded, edge_add refused a pair id for claim 550 as "not in session" — a claim mole itself had stored minutes earlier. The eval reads whole sessions with the same magnitude.
const AskAllowance = 50_000 // $0.05
AskAllowance bounds what one research.ask call may spend, in micro-dollars.
§13's promise is that an ask is cheap: one model call against claims already paid for. The allowance is what makes that true rather than aspirational — without it an ask draws on whatever the session has left, and a session that stopped early with most of its budget intact could spend it all answering one follow-up.
const DefaultAskTimeout = 5 * time.Minute
DefaultAskTimeout bounds an ask's wall clock. One model call; five minutes is generous for it and short enough that a wedged provider does not pin a reservation until the abandonment sweep. Enforced on the context in Ask.
const DefaultSessionsListed = 20
DefaultSessionsListed is the page size for research.sessions.list.
const DefaultToolkitBudgetUSD = 1_000_000 // micros
DefaultToolkitBudgetUSD is what a session gets when the caller names no budget: one dollar, which is roughly 125 Tavily queries.
const MaxClaimsReturned = 200
MaxClaimsReturned caps how many claims research.result hands back.
The result goes straight into a coding agent's context window, and nothing upstream bounds how many claims a session gathers. A cap the caller can see — reported as Truncated with the real Total — beats both silently sending five thousand and silently sending two hundred of them.
const MaxDatasetRows = 200
MaxDatasetRows bounds what one mole.dataset call returns.
The merged table goes into an agent's context window twice over — as markdown and as structured rows — so it needs the same bound the other list tools have. `mole dataset` exports the whole thing.
const MaxEdgesReturned = 500
MaxEdgesReturned caps the edges alongside them, for the same reason.
const MaxFetchChars = 120_000
MaxFetchChars bounds the text one fetch returns.
The result goes straight into an agent's context window, and nothing about a web page bounds its length. Truncation is reported rather than silent, because a quote from beyond the cut will fail verification later and the caller needs to know that is why.
const MaxPairsReturned = 50
MaxPairsReturned bounds one retrieval. Pairs go straight into an agent's context window, and a session with two hundred claims has thousands of candidate pairs.
const MaxRowsPerCall = 200
MaxRowsPerCall bounds one batch. Larger batches are not refused work, only split, and a bound keeps one call from holding a write transaction open over an arbitrary amount of parsing.
const MaxSearchResults = 10
MaxSearchResults bounds one search for the same reason.
const MaxTitleChars = 300
MaxTitleChars bounds a page-supplied title.
Titles reach the model OUTSIDE the fence — they are metadata a caller cites by, not document text — so the only protection available is that they are short. Same bound as a search snippet, for the same reason.
const MaxToolkitCalls = 500
MaxToolkitCalls bounds how many searches and fetches one toolkit session may make.
Autonomous mode is bounded by money: every call is on a lead, and leads stop when the budget does. Here the money bound is weak on purpose — a search and a fetch cost mole no model tokens, so in a token-budget session the ledger would never stop an agent that loops. This is the ceiling that does, and it is §8.5's existing one rather than a new mechanism.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type AskOut ¶
type AskOut struct {
SessionID string `json:"session_id"`
Question string `json:"question"`
Answer string `json:"answer"`
// Claims are the ones the answer is built from, so a caller can weigh the
// evidence rather than the prose (§5.3).
Claims []Claim `json:"claims"`
// Citations maps each [n] in the answer to its source.
Citations []Citation `json:"citations"`
// AskSessionID is the small session this answer was charged to. Present so
// the cost is traceable: it will not appear against the session that was
// queried, whose accounts are closed.
AskSessionID string `json:"ask_session_id,omitempty"`
Spent int64 `json:"spent"`
// Degraded says why the answer is not synthesized prose, when it is not.
Degraded string `json:"degraded,omitempty"`
}
type Budget ¶
type Budget struct {
Unit string `json:"unit" jsonschema:"the budget unit: usd or tokens"`
Amount string `json:"amount" jsonschema:"how much to spend: dollars like 2.50 for usd, a whole number of tokens for tokens"`
}
Budget is the amount a session may spend. Both fields are required: §8 makes the unit semantically load-bearing, and a bare number is ambiguous between dollars and tokens.
type Citation ¶
type Citation struct {
N int `json:"n"`
Source string `json:"source"`
Quotes []string `json:"quotes,omitempty"`
PublishedAt string `json:"published_at,omitempty"`
}
Citation is one numbered source behind an answer.
Quotes plural: several claims can cite one source, and §13 asks for the verified span per citation so a reader can check the answer without re-fetching the page.
type Claim ¶
type Claim struct {
ID string `json:"id"`
Text string `json:"text"`
Source string `json:"source"`
Quote string `json:"quote,omitempty"`
PublishedAt string `json:"published_at,omitempty"`
Confidence float64 `json:"confidence"`
Grounded *bool `json:"grounded,omitempty"`
}
Claim is one finding, in the shape §5.3 says an agent caller wants: sourced, dated, quoted, and scored, rather than a paragraph.
type Deps ¶
type Deps struct {
// Supervisor starts and cancels sessions.
Supervisor *session.Supervisor
// Store answers every read. Deliberately not the supervisor: the store is
// authoritative and survives a restart, while the supervisor only remembers
// the sessions it is running plus a bounded tail of finished ones. A status
// query that worked before a daemon restart and failed after would be the
// worst possible behaviour for a caller told to poll.
Store store.Store
// MaxSessionUSD and MaxSessionTokens cap a single session's budget in their
// respective units. Zero means no ceiling. See config.MaxSessionUSD for why
// these exist, and checkCeiling for why having only one set is refused.
MaxSessionUSD int64
MaxSessionTokens int64
// Version is the binary's version, reported to MCP clients in the server
// handshake. Empty reports "dev".
Version string
// LLM answers research.ask. Nil disables synthesis: an ask then returns the
// relevant claims with citations and no prose, which is the honest response
// when no call can be made.
LLM llmProvider
// Pricing turns an ask's token usage into a ledger cost. Nil takes the
// default table.
Pricing *pricing.Table
// AskTimeout bounds one research.ask. Zero takes DefaultAskTimeout.
AskTimeout time.Duration
// Workers is how many leads each session runs at once. Zero takes
// executor.DefaultWorkers.
Workers int
// Defaults applied when a call does not specify.
MaxSources int
MaxDepth int
MaxLeads int
Timeout time.Duration
// Search, Fetch and Extract back toolkit mode. Nil leaves the matching tool
// reporting that it is not configured rather than failing at call time.
Search search.Provider
Fetch fetch.Fetcher
Extract extract.Extractor
// Connectors is the registered local data, for the toolkit's aggregate tools.
// Nil leaves them reporting that nothing is registered.
Connectors interface{ List() []connector.Connector }
// Gate tunes the aggregation gate. The zero value is §12.1's defaults.
Gate gate.Options
// Toolkit registers the toolkit tool surface. Off by default: those tools cost
// every client their definitions in its context window, and a caller who wants
// autonomous research should not read past them to find research.report.
Toolkit bool
Log *slog.Logger
}
Deps is what the tools operate on.
func (Deps) Ask ¶
ask answers a new question from a finished session's graph (§13).
The ask runs as its OWN session, with mode "ask" and a small budget of its own, reading the source session's claims. That is not indirection for its own sake — it is forced, and the reason is worth stating.
§8 refuses a reservation against a terminal session, and rightly: a finished session's ledger is settled and reconciled, and adding cost rows to it later rewrites a closed account. So an ask cannot be charged to the research it queries. Giving it a session of its own keeps every §8 invariant intact — every model call belongs to a live session, every cost row has a reservation, the source session's accounts stay closed — and has the side benefit that an ask shows up in sessions.list with its own cost, which is what a person wondering where the money went needs to see.
This is also what core.ModeAsk was reserved for. §13 describes it as "listed in rev 1's Session.Mode but never defined"; this defines it.
Exported because `mole ask` is the same operation from a terminal instead of an MCP client, and the budgeting above is the part that must not be written twice. The CLI builds a Deps with no Supervisor — an ask never starts one — and calls this directly.
type Edge ¶
type Edge struct {
From string `json:"from"`
To string `json:"to"`
Kind string `json:"kind"`
Weight float64 `json:"weight,omitempty"`
Rationale string `json:"rationale,omitempty"`
}
Edge is one relation in the claim graph (§11.2).
type ListIn ¶
type ListIn struct {
Limit int `json:"limit,omitempty" jsonschema:"how many recent sessions to return; omit for 20"`
}
type ListOut ¶
type ListOut struct {
Sessions []SessionSummary `json:"sessions"`
}
type ReportIn ¶
type ReportIn struct {
Prompt string `json:"prompt" jsonschema:"the research question"`
Budget Budget `json:"budget" jsonschema:"what this session may spend"`
MaxSources int `json:"max_sources,omitempty" jsonschema:"sources to read per lead; omit for the daemon default"`
MaxDepth int `json:"max_depth,omitempty" jsonschema:"rounds of follow-up questions the planner may add; omit for the daemon default"`
}
type ResultOut ¶
type ResultOut struct {
SessionID string `json:"session_id"`
Status string `json:"status"`
ReportMD string `json:"report_md"`
Claims []Claim `json:"claims"`
Edges []Edge `json:"edges"`
// TotalClaims and TotalEdges are what the session holds; the arrays above may
// be shorter. Reported so a truncated result is visibly truncated — silently
// returning a prefix is how a caller concludes a session found less than it
// did.
TotalClaims int `json:"total_claims"`
TotalEdges int `json:"total_edges"`
Truncated bool `json:"truncated"`
Note string `json:"note,omitempty"`
}
type SessionRef ¶
type SessionRef struct {
SessionID string `json:"session_id" jsonschema:"the id returned by research.report"`
}
type SessionSummary ¶
type StatusOut ¶
type StatusOut struct {
SessionID string `json:"session_id"`
Status string `json:"status"`
Prompt string `json:"prompt"`
Unit string `json:"budget_unit"`
Budget int64 `json:"budget"`
Spent int64 `json:"spent"`
Remaining int64 `json:"remaining"`
Claims int `json:"claims"`
Verified int `json:"claims_verified"`
Edges int `json:"edges"`
Running bool `json:"running"`
}