npm i ideate-core
# Zero-setup smoke test — folds human ideas with no model client or API key:
echo '{"context":{"slug":"demo"},"humanIdeas":["a seed idea","another"]}' | npx ideateThen wire your own model client to generate ideas — the minimal ideateCore call
is a few lines (full example below). The rest of this README explains
what the engine does and why; jump straight to Install for the API.
Use case: you need a pool of genuinely different ideas from an LLM — for a campaign brief, a product-naming pass, a strategy option set — not five rephrasings of the same idea with the temperature turned up.
Differentiator: most "ideation" wrappers are one model call with a "be creative" system prompt. "Ask the model for 5 ideas" reliably produces 5 idea phrasings. ideate-core instead engineers the diversity the way human brainwriting does: independent, blind generator agents (persona is the lever, not temperature — the research backs this), pooled through real build-on rounds, then optionally converged with embedding-dedup + split-axis scoring instead of one LLM-judge "best idea." It's a zero-dependency injectable function, not a framework — bring your own model client.
A provider-agnostic, evidence-based ideation engine — independent multi-agent generation, blind→pool brainwriting rounds, a divergent→convergent selection half, and an evaluate→regenerate feedback loop, as a zero-dependency injectable function. Not a framework, not model-locked: you bring the model client, the embedder, and the prompts.
Turns a domain context into a pool of idea candidates by running a panel of independent generator agents (each a separate model call with no shared context — the nominal-group analog), optionally running an expansion round over round one, and folding in any human-supplied ideas so they ride the same downstream gates.
Every research claim below (Wang et al. 2023, Meincke et al. 2024, Rohrbach 1968, …) is sourced in full — author + year + URL — in docs/ideation-method.md, alongside a defaults→evidence table.
- Independent multi-agent round 1 — N agents (default 5), each a separate blind model call. Diversity is engineered via per-agent levers — persona (default: pragmatist / contrarian / domain-expert / outsider-analogy / visionary), prompt strategy (chain-of-thought), and, where the model supports it, temperature. "Be diverse" alone fails; persona beats temperature as a lever (Wang et al. 2023; Meincke et al. 2024). Note: current Anthropic frontier models (Opus 5, Sonnet 5, Opus 4.8/4.7, Fable 5) reject
temperature/top_p/top_k; Haiku 4.5 still accepts them. The example adapters strip a rejected sampling param and warn (docs/ideation-method.md §2), so persona is the primary structural lever on frontier models. - Cross-provider panel — route each agent to a different provider/model via an injected
clientsmap orresolveClientresolver (Anthropic + OpenAI + xAI/Grok + …). No vendor SDK is baked in; heterogeneous models give real variance and sidestep self-preference bias (Wataoka et al. 2024). - Per-agent
effort— an optionaleffortfield on an agent spec (deps.agents[i].effort), passed straight through to that agent'scomplete(req)call asreq.effort. It is assigned, not merged with a fallback the waypersona/modelare (spec.X || base.X) — soeffort: null,0, or""reachreq.effortunchanged instead of falling back to a default, and an agent that never set it stays distinguishable (undefined) from one that explicitly set a falsy value. The engine does not validate or interpreteffort— it is opaque cargo; the meaningful ladder (e.g.low/medium/high/xhigh/max) is defined by whatever adapter or provider reads it. Both bundled adapters forward it:integrations/subagent-dispatchincludes it in the defaultmapRequesttask shape,integrations/headless-cliforwards it as--effort <value>. - Build-on rounds with a sharing policy — round 1 is blind; build-on rounds (2+) default to pool sharing: agents build on the shared, deduped pool, not their own seeds — real brainwriting / 6-3-5 (Rohrbach 1968; Paulus & Yang 2000). Dedupe-before-share is mandatory for pool rounds (a raw pool triggers fixation — Kohn & Smith 2011). Per-round
sharing,incubation,buildOnDirective, andmaxRoundsare all config-driven. - Human-idea folding — caller ideas are normalized into the candidate shape and merged; a near-identical human idea wins the dedup tie.
- Robust parsing — tolerates ```json fences, surrounding prose, and
{candidates|ideas|posts:[...]}wrappers; drops malformed candidates rather than throwing. - Convergence (opt-in,
ideate-core/converge) — the divergent→convergent second half: embedding-cosine dedup (default 0.83; collapses semantic near-dups a text key misses), clustering (k auto) so selection samples across themes, split-axis scoring (novelty ⟂ feasibility, never one "best" — Rietzschel et al. 2010), a cross-cluster shortlist, a human-rerank hook (LLM-judge is a filter, not a novelty ranker — Zheng et al. 2023), and a diversity metric vs a floor. The embedder is injected (offline-mockable). - Evaluate→regenerate feedback loop (opt-in,
ideate-core/feedback) — a Delphi-style controlled-feedback loop (Dalkey & Helmer 1963): an injected external evaluator (panelistis the intended first one) critiques the pool, and only the flagged ideas are targeted-regenerated against their specificdealKillers/keepReasons;keeppasses,killdrops,reviseregenerates, then the pool re-dedupes. The evaluator model must differ from the generators (self-preference bias — Wataoka et al. 2024). The feedback-in contract itself is provider-agnostic; seeexampleAdapterFromPanelistinlib/feedback.mjsfor a worked example of adapting one evaluator's output shape onto it — the adapter is illustrative, not a required or canonical format. - Global dedup, provider-agnostic injectable client + embedder (tests stay offline), zero domain code.
A per-agent complete() call that throws or returns a bad reply is dropped, not
fatal — one bad model reply must never sink the run. But if it dropped
silently, a caller couldn't tell "the models produced no ideas" from "nothing
ran at all." ideateCore never throws on this; instead it makes the failure
observable on the return value, with no preflight required:
meta.agentsAttempted/meta.agentsFailed— round-1 panel size and how many of those calls failed (threw, returned a bad reply, or had no resolvable client).agentsFailed === agentsAttemptedwith an emptycandidatesarray means every round-1 agent failed — the pool is empty because of that, not because the models had no ideas.meta.agentErrors— the reason behind each failure, recorded by default across every round (round 1 and any build-on round), whether or not you passdeps.onAgentError. Two entry shapes, discriminated bykind:{kind: "agent", agentId, round, message}— an agent's own call failed (threw, returned a bad reply, or had no resolvable client). Note the scope difference:agentsFailedcounts round-1 nulls only, whileagentErrorsspans all rounds —agentErrors.filter(e => e.round === 1).length === agentsFailedalways holds.{kind: "callback", agentId, agentRound, message}— your owndeps.onAgentErrorthrew synchronously while reacting to the entry above. KeyedagentRound, notround, so it never counts toward the invariant above.
deps.onAgentError(err, {agentId, round})— an optional callback fired once per failure (same rounds asagentErrors) if you want to react to a failure as it happens rather than inspectingmetaafterward; it runs alongside the default recording, not instead of it. A synchronous throw can never fail the run — it is contained and recorded askind: "callback"above. An async callback that rejects (e.g.onAgentError: async (e) => { await logToService(e); }where theawaitthrows) is also contained — it can never crash the process via an unhandled rejection — but the rejection is not recorded inmeta.agentErrors: it settles afterideateCoremay have already returned, and there is no way to fold it intometawithout either blocking the run on your callback or mutating an object you already hold. If you need to observe an async failure reliably, do your owntry/catch/logging inside the callback rather than relying on the return value.
npm i ideate-coreApache-2.0, published to the public npm registry (zero runtime dependencies, Node.js >= 20). Bring your own model client, embedder, and prompts.
Prerequisite — you supply your own complete() model-calling function (no API
client is bundled). Two shapes are contractual, and getting either wrong is
silently dropped — you get candidates: [] with no error thrown:
complete(req)must resolve to{ ok: true, text: string }. Anything else (a bare string,{ text }withoutok: true, or a thrown error) yields no candidate.req.promptis the string yourbuildRound1Promptreturned.- Each candidate in the model's JSON reply must be shaped
{ text: "..." }(a non-empty stringtext) — not{ title, body }. A bare JSON array, or a{ candidates | posts | ideas: [...] }wrapper, are all accepted; any object without a non-empty stringtextis dropped.
Input validation is the caller's job. ideate-core does not validate the
shape of input.context — the engine passes it straight through to your
buildRound1Prompt/prompt builders, so shape-validating context (and any other
input fields your adapter reads) is the calling adapter's responsibility, not the
engine's. This is intentional: the engine stays domain-agnostic and never throws on
your input.
import { ideateCore } from "ideate-core";
// ── You bring two functions; ideate-core calls YOUR model through them. ──
// 1) complete(req) MUST resolve to { ok: true, text: string }. `text` is the
// model's raw reply. Anything else is silently dropped (candidates: []).
const complete = async (req) => {
const text = await callYourModel(req.prompt); // your provider call
return { ok: true, text }; // ← required return shape
};
// 2) buildRound1Prompt({ context, stance, persona, ... }) returns the prompt
// string. Tell the model to reply with candidate objects each shaped
// { "text": "..." } — NOT { title, body }.
const buildRound1Prompt = ({ context, stance }) =>
`${stance ?? ""}\nGive 6 ideas for: ${context.brief}.\n` +
`Reply ONLY with a JSON array like [{"text": "first idea"}, {"text": "second idea"}].`;
// Single-client panel (the default 5 personas all route to one client):
const { candidates } = await ideateCore(
{ context: { slug: "demo", brief: "ways to promote a launch" } },
{ complete, buildRound1Prompt /* , buildRound2Prompt, normalizeExtra */ },
);
// Cross-provider panel — one agent per provider (each client is its own
// `complete(req) => { ok: true, text }`, same contract as above):
const { candidates: pool } = await ideateCore(
{ context: { slug: "demo", brief: "…" } },
{
buildRound1Prompt,
clients: { "claude-x": anthropicComplete, "gpt-x": openaiComplete },
agents: [
{ persona: "pragmatist", model: "claude-x" },
{ persona: "contrarian", model: "gpt-x" },
],
},
);A thin standalone CLI (bin/ideate.mjs, installed as ideate) wraps the same
engine for shell/pipeline use:
echo '{"context":{"slug":"demo"},"humanIdeas":["a seed idea"]}' \
| ideate --adapter ./my-adapter.mjs # ESM module exporting `deps` for ideateCore
ideate --version # print the installed version
ideate --help # full usageWithout --adapter the CLI runs fold-only: it folds humanIdeas from stdin
and prints them, no model client or API key required — useful for sanity-checking
the human-idea path before wiring a real adapter.
ideate-core bundles a small set of optional, interchangeable example
adapters under integrations/. Each supplies a complete(req) => { ok, text } implementation you can drop into deps.complete — they are
worked examples, not core dependencies: the engine has no import-time
dependency on any of them, and your own HTTP client is equally first-class.
- headless-CLI (
ideate-core/integrations/headless-cli) — runscomplete()against a locally-authenticated headless Claude Code CLI session (claude -p --output-format json) instead of a metered API key, so any Claude Code user can ideate on their existing session auth. Fails loudly (preflight + throwingcomplete) when the CLI is missing/unauthenticated — never a silent empty pool. - subagent-dispatch (
ideate-core/integrations/subagent-dispatch) — maps round 1's N independent persona agents onto a host's own subagent / Task-dispatch primitive (one dispatch per persona), a natural fit for agent runtimes like Claude Code's interactive installs. Fails loudly (construction throws + preflight) when no dispatch capability is wired — never a silent empty pool.
Each adapter ships hermetic tests (the subprocess/dispatch primitive is injected, so no real CLI, agent runtime, or network is needed to pass CI).
Writing your own adapter? The package bundles a self-contained, user-invocable
skill — skills/adapter-authoring — that
walks through the complete() / candidate {ok, text} contract, a minimal
adapter from scratch, the loud-failure discipline, and both bundled adapters as
worked examples.
ideate-core is a small evidence-based pipeline that diverges then
converges — independent multi-agent generation, blind→pool build-on rounds,
embedding dedup + clustering + split novelty/feasibility selection, and an
optional Delphi-style evaluate→regenerate loop.
The full rationale lives in one place: docs/ideation-method.md is the single source of truth for why the engine is shaped this way — every design decision and default justified by a cited finding (author + year + URL), plus a "defaults & their evidence" table. Start there.
Honesty note: synthetic ideation is a drafting aid, not a substitute for real customer discovery — treat its shortlist as hypotheses to test with people, not answers.
Developed by Kromatic for internal use, then open sourced (Apache-2.0) and published to public npm. The configurable multi-agent engine (independent generators + blind→pool build-on rounds, nominal-group / brainwriting style), the divergent→convergent selection half, and the generate→evaluate→regenerate feedback loop are all implemented (feature-complete) — see the method doc above.
Stability: ideate-core is pre-1.0 (0.x) — feature-complete, but the public API may still change before 1.0 (per the versioning convention: while the major version is 0, a 0.x.0 minor may carry breaking changes). See SECURITY.md and CONTRIBUTING.md; the exact published version is shown by the npm badge above.
