Skip to content
Night Owls.dev
Jump to a page

@nightowlsdev/agent-researcher

Pre-built agents

Deep 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-researcher

Key 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

agent-researcher.ts
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-agents

What 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.

agent-researcher-example-1.ts
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.

agent-researcher-example-2.ts
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.

agent-researcher-example-3.ts
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-kitThe curated-skill import + verify + library helpers every pre-built agent shares.
  • knowledgeThe corpus store behind corpusOnly mode — searchKnowledgeTool(store) is the knowledgeSearch arm.
  • connectorsA connector action is one way to supply the webSearch / fetchUrl tools.
  • agent-marketerComposes this researcher as a delegate for market/competitor source-gathering.
  • adopt-prebuilt-agentsThe full four-step host-wiring journey (storage, tier config, approvals).