@nightowlsdev/agent-researcher
Pre-built agentsDeep research with a fail-loud tool contract: plans sub-questions with stop criteria, searches broad-then-narrow, triages sources primary-first, gap-checks, and delivers a cited synthesis.
What it does
@nightowlsdev/agent-researcher productizes the deep-research playbook: plan with explicit sub-questions and stop criteria → start broad then narrow → triage sources (primary first, dated; two independent sources for load-bearing claims) → compress findings at boundaries → gap-check → a SEPARATE citation pass where an unattributable claim is rewritten as an open question, never left as fact. There is no framework web search, you inject the tools (an MCP server, a custom defineTool, a connector action) and the factory THROWS at build time when the required arm is missing. `corpusOnly` is the explicit degraded-but-grounded mode: it requires a knowledgeSearch tool (e.g. searchKnowledgeTool from @nightowlsdev/knowledge), excludes every live-web tool even if supplied, and appends a persona addendum naming the corpus as the retrieval boundary. Curated skills.sh refs (pinned): a vendor-free core granted by default plus a needs-vendor-key optional set (firecrawl/tavily/parallel).
Install
pnpm add @nightowlsdev/agent-researcherKey exports
- createResearcher ({ tools: { webSearch, fetchUrl?, knowledgeSearch? }, corpusOnly? })
- manifest / RESEARCHER_CURATED_SKILLS (vendor-free core + needs-vendor-key set, pinned)
- RESEARCHER_PERSONA / CORPUS_ONLY_PERSONA_ADDENDUM
Usage
import { createResearcher, manifest } from "@nightowlsdev/agent-researcher";
import { importCuratedSkills } from "@nightowlsdev/agent-kit";
import { materializeSkillStore, skillsShProvider } from "@nightowlsdev/skills";
import { defineSwarm } from "@nightowlsdev/core";
// The host wires the tools, the factory FAILS LOUD when the required arm is missing.
const researcher = createResearcher({
tools: { webSearch: myWebSearchTool, fetchUrl: myFetchTool }, // MCP server / defineTool / connector action
// corpusOnly: true + knowledgeSearch: searchKnowledgeTool(store), the no-live-web mode
});
// Curated skills import once per tenant; dynamicSkills injects them (fenced) at runtime.
await importCuratedSkills({ sets: manifest.curatedSkills, providers: { "skills.sh": skillsShProvider() }, storage, tenantId, actor });
const swarm = defineSwarm({
storage,
agents: [researcher],
dynamicSkills: materializeSkillStore(storage.skills), // REQUIRED for stored-skill grants
models: {
allow: ["openai/gpt-5.5-mini", "openai/gpt-5.5"],
tier: { tiers: { swift: "openai/gpt-5.5-mini" }, default: "swift" }, // "tier:" needs a tier config
},
modelFactory,
cost: { maxSteps: 30, maxCostUsd: 0.5 },
});
// The full journey: /docs/adopt-prebuilt-agentsWhat it provides
agent-researcher is a pre-built deep-research agent with a fail-loud tool contract: it plans sub-questions with explicit stop criteria, searches broad-then-narrow, triages primary sources, gap-checks, and delivers a cited synthesis in a SEPARATE citation pass where a claim that can't be attributed is rewritten as an open question — never left as fact. There is no framework web search: you inject the retrieval tools, and the factory throws at build time when the required arm is missing.
When to use it
- You need rigorous, cited research over live web (or a private corpus) and want the source-triage / two-independent-sources / separate-citation-pass discipline out of the box.
- You have a web-search tool to inject — an MCP server, a custom defineTool, or a connector action.
- You want a grounded no-live-web mode over your own knowledge corpus (corpusOnly).
When not to
- You have no retrieval tool at all — the factory deliberately throws rather than answer from priors; inject a webSearch or knowledgeSearch tool first.
- You want a general chat/assistant agent — this persona is narrow (research) and the tool contract is strict.
- You only need a single quick lookup — a raw tool call is cheaper than the whole plan → triage → cite loop.
Alternatives
- A hand-authored agent with a search skillYou want a lighter research persona without the enforced citation pass / stop criteria, or you're folding research into a larger custom agent.
- agent-marketer's researcher delegateYour research need is specifically competitor/market source-gathering feeding a marketing analysis — createMarketingCrew wires this researcher as a delegate.
Strengths
- Fail-loud by construction: createResearcher throws at build time via assertToolRequirements when the required arm (webSearch, or knowledgeSearch under corpusOnly) is missing — no silently under-tooled agent.
- The deep-research persona IS the product: effort scaling, two independent sources for load-bearing claims, a separate citation pass, and 'I could not verify X' as a first-class finding.
- corpusOnly draws a hard live-web boundary — it excludes every web tool even if supplied (including fetchUrl, which is live web), requires knowledgeSearch, and appends a persona addendum naming the corpus as the retrieval limit.
- Retrieved text is treated as reference material, never as instructions — a prompt-injection guardrail baked into the persona.
- Vendor-free curated core granted by default; a needs-vendor-key optional set (firecrawl / tavily / parallel) you grant only when you hold the keys.
Limits & trade-offs
- You must supply and govern the retrieval tools yourself — the package ships none, and result quality tracks the search tool you inject.
- Single-loop by default: no built-in lead + parallel sub-researcher delegation; compose the multi-agent shape via delegates/bundles if you want it.
- No turnkey eval suite (citation-coverage scorers) yet — blocked on FR-018.
- Depth costs tokens: the plan → broad → narrow → gap-check → cite loop is deliberately more expensive than a one-shot answer; cap it with cost.maxSteps / maxCostUsd.
How it works
createResearcher({ tools, corpusOnly? }) validates the required tool arm at factory time and grants exactly the tools that fit the mode — under corpusOnly it grants neither webSearch nor fetchUrl and requires knowledgeSearch, otherwise it grants the web arm(s). It merges the deep-research persona (plus the corpus addendum when corpusOnly) with your PrebuiltAgentOpts into an AgentDef whose default grants are the vendor-free curated core. You import that curated set once per tenant with importCuratedSkills and inject it at runtime with dynamicSkills: materializeSkillStore(...); without that injection the stored grants are inert.
Examples
Live-web researcher
Inject the tools; the factory THROWS if webSearch is missing.
import { createResearcher } from "@nightowlsdev/agent-researcher";
const researcher = createResearcher({
tools: {
webSearch: myWebSearchTool, // MCP server / defineTool / connector action — REQUIRED
fetchUrl: myFetchTool, // optional close reading
},
});Corpus-only, no live web
knowledgeSearch becomes REQUIRED; web tools are excluded even if supplied.
import { createResearcher } from "@nightowlsdev/agent-researcher";
import { searchKnowledgeTool } from "@nightowlsdev/knowledge";
const researcher = createResearcher({
corpusOnly: true,
tools: { knowledgeSearch: searchKnowledgeTool(store) },
});Import curated skills and wire into a swarm
dynamicSkills is REQUIRED — the vendor-free core grant is inert without it.
import { createResearcher, manifest } from "@nightowlsdev/agent-researcher";
import { importCuratedSkills } from "@nightowlsdev/agent-kit";
import { materializeSkillStore, skillsShProvider } from "@nightowlsdev/skills";
import { defineSwarm } from "@nightowlsdev/core";
await importCuratedSkills({ sets: manifest.curatedSkills, providers: { "skills.sh": skillsShProvider() }, storage, tenantId, actor });
const swarm = defineSwarm({
storage,
agents: [createResearcher({ tools: { webSearch: myWebSearchTool } })],
dynamicSkills: materializeSkillStore(storage.skills),
models: { allow: ["openai/gpt-5.5-mini"], tier: { tiers: { swift: "openai/gpt-5.5-mini" }, default: "swift" } },
modelFactory,
cost: { maxSteps: 30, maxCostUsd: 0.5 },
});Doing the parts it doesn't support
- Web search itselfThe package ships none. Inject an MCP server, a custom defineTool, or a connector action as tools.webSearch — the persona is designed around a search arm you own and govern.
- A lead + parallel sub-researchers crewThe researcher is single-loop by default. Compose the multi-agent shape with delegates / a bundle — publish several researcher instances and a lead that delegates to them.
- Grounding without any live webSet corpusOnly: true and pass a knowledgeSearch tool (e.g. searchKnowledgeTool from @nightowlsdev/knowledge). Web tools are then excluded even if supplied, and the persona names the corpus as its boundary.
Related
- agent-kit — The curated-skill import + verify + library helpers every pre-built agent shares.
- knowledge — The corpus store behind corpusOnly mode — searchKnowledgeTool(store) is the knowledgeSearch arm.
- connectors — A connector action is one way to supply the webSearch / fetchUrl tools.
- agent-marketer — Composes this researcher as a delegate for market/competitor source-gathering.
- adopt-prebuilt-agents — The full four-step host-wiring journey (storage, tier config, approvals).